From d647adc0b6f6692c64c52f120d51ef5c669baeb5 Mon Sep 17 00:00:00 2001 From: Jr7468 Date: Wed, 28 May 2025 22:50:51 +0100 Subject: [PATCH 001/160] feat: Add mail contact deployment standard script Add new PowerShell script for deploying mail contacts in CIPP standards. This script provides functionality to manage and deploy mail contact configurations across tenant environments. Tweak to prevent early code exit Further adjustment to code execution Streamlined --- .../Invoke-CIPPStandardDeployMailContact.ps1 | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1 diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1 new file mode 100644 index 000000000000..1802ac42847a --- /dev/null +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1 @@ -0,0 +1,103 @@ +function Invoke-CIPPStandardDeployMailContact { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) DeployMailContact + .SYNOPSIS + (Label) Deploy Mail Contact + .DESCRIPTION + (Helptext) Creates a new mail contact in Exchange Online across all selected tenants. The contact will be visible in the Global Address List. + (DocsDescription) This standard creates a new mail contact in Exchange Online. Mail contacts are useful for adding external email addresses to your organization's address book. They can be used for distribution lists, shared mailboxes, and other collaboration scenarios. + .NOTES + CAT + Exchange Standards + TAG + ADDEDCOMPONENT + {"type":"textField","name":"standards.DeployMailContact.ExternalEmailAddress","label":"External Email Address","required":true} + {"type":"textField","name":"standards.DeployMailContact.DisplayName","label":"Display Name","required":true} + {"type":"textField","name":"standards.DeployMailContact.FirstName","label":"First Name","required":false} + {"type":"textField","name":"standards.DeployMailContact.LastName","label":"Last Name","required":false} + IMPACT + Low Impact + ADDEDDATE + 2025-05-28 + POWERSHELLEQUIVALENT + New-MailContact + RECOMMENDEDBY + "CIPP" + #> + + param($Tenant, $Settings) + + # Input validation + if ([string]::IsNullOrWhiteSpace($Settings.DisplayName)) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'DeployMailContact: DisplayName cannot be empty or just whitespace.' -sev Error + return + } + + try { + $null = [System.Net.Mail.MailAddress]::new($Settings.ExternalEmailAddress) + } + catch { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "DeployMailContact: Invalid email address format: $($Settings.ExternalEmailAddress)" -sev Error + return + } + + # Prepare contact data for reuse + $ContactData = @{ + DisplayName = $Settings.DisplayName + ExternalEmailAddress = $Settings.ExternalEmailAddress + FirstName = $Settings.FirstName + LastName = $Settings.LastName + } + + # Check if contact already exists + try { + $ExistingContact = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-MailContact' -cmdParams @{ + Identity = $Settings.ExternalEmailAddress + ErrorAction = 'Stop' + } + } + catch { + if ($_.Exception.Message -like "*couldn't be found*") { + $ExistingContact = $null + } + else { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Error checking for existing mail contact: $(Get-CippException -Exception $_).NormalizedError" -sev Error + return + } + } + + # Remediation + if ($Settings.remediate -eq $true -and -not $ExistingContact) { + try { + $NewContactParams = $ContactData.Clone() + $NewContactParams.Name = $Settings.DisplayName + $null = New-ExoRequest -tenantid $Tenant -cmdlet 'New-MailContact' -cmdParams $NewContactParams + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully created mail contact $($Settings.DisplayName) with email $($Settings.ExternalEmailAddress)" -sev Info + } + catch { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Could not create mail contact. $(Get-CippException -Exception $_).NormalizedError" -sev Error + } + } + + # Alert + if ($Settings.alert -eq $true) { + if ($ExistingContact) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Mail contact $($Settings.DisplayName) already exists" -sev Info + } + else { + Write-StandardsAlert -message "Mail contact $($Settings.DisplayName) needs to be created" -object $ContactData -tenant $Tenant -standardName 'DeployMailContact' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Mail contact $($Settings.DisplayName) needs to be created" -sev Info + } + } + + # Report + if ($Settings.report -eq $true) { + $ReportData = $ContactData.Clone() + $ReportData.Exists = [bool]$ExistingContact + Add-CIPPBPAField -FieldName 'DeployMailContact' -FieldValue $ReportData -StoreAs json -Tenant $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.DeployMailContact' -FieldValue $($ExistingContact ? $true : $ReportData) -Tenant $Tenant + } +} \ No newline at end of file From 912648e08702808b6a261c395e9392d828cd7e09 Mon Sep 17 00:00:00 2001 From: ngms-psh Date: Fri, 30 May 2025 18:03:31 +0200 Subject: [PATCH 002/160] Fixed issue with compare. Used wrong input for convert --- .../Spamfilter/Invoke-ListQuarantinePolicy.ps1 | 2 -- .../Standards/Invoke-CIPPStandardQuarantineTemplate.ps1 | 5 ++++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListQuarantinePolicy.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListQuarantinePolicy.ps1 index 55b13d960f3a..7cc4ed4204d5 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListQuarantinePolicy.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListQuarantinePolicy.ps1 @@ -18,8 +18,6 @@ function Invoke-ListQuarantinePolicy { $Policies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-QuarantinePolicy' -cmdParams @{QuarantinePolicyType=$QuarantinePolicyType} | Select-Object -Property * -ExcludeProperty *odata*, *data.type* - write-host $($Request | ConvertTo-Json -Depth 10) - if ($QuarantinePolicyType -eq 'QuarantinePolicy') { # Convert the string EndUserQuarantinePermissions to individual properties $Policies | ForEach-Object { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineTemplate.ps1 index 566754a66bdc..83410760f70f 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineTemplate.ps1 @@ -52,6 +52,9 @@ function Invoke-CIPPStandardQuarantineTemplate { try { # Create hashtable with desired Quarantine Setting $EndUserQuarantinePermissions = @{ + # ViewHeader and Download are set to false because the value 0 or 1 does nothing per Microsoft documentation + PermissionToViewHeader = $false + PermissionToDownload = $false PermissionToBlockSender = $Policy.PermissionToBlockSender PermissionToDelete = $Policy.PermissionToDelete PermissionToPreview = $Policy.PermissionToPreview @@ -64,7 +67,7 @@ function Invoke-CIPPStandardQuarantineTemplate { if ($Policy.displayName.value -in $CurrentPolicies.Name) { #Get the current policy and convert EndUserQuarantinePermissions from string to hashtable for compare $ExistingPolicy = $CurrentPolicies | Where-Object -Property Name -eq $Policy.displayName.value - $ExistingPolicyEndUserQuarantinePermissions = Convert-QuarantinePermissionsValue @EndUserQuarantinePermissions -ErrorAction Stop + $ExistingPolicyEndUserQuarantinePermissions = Convert-QuarantinePermissionsValue -InputObject $ExistingPolicy.EndUserQuarantinePermissions -ErrorAction Stop #Compare the current policy $StateIsCorrect = ($ExistingPolicy.Name -eq $Policy.displayName.value) -and From 7a1de2a0a24efa4c62dca1d779e1c463dd540440 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Sat, 31 May 2025 00:30:13 +0200 Subject: [PATCH 003/160] fix: fix adding user from Add to group useraction --- .../Identity/Administration/Groups/Invoke-EditGroup.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-EditGroup.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-EditGroup.ps1 index ada79c9ca21f..37e88f23886f 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-EditGroup.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-EditGroup.ps1 @@ -33,7 +33,8 @@ function Invoke-EditGroup { if ($AddMembers) { $AddMembers | ForEach-Object { try { - $member = $_.value + # Add to group user action and edit group page sends in different formats, so we need to handle both + $Member = $_.value ?? $_ $memberid = $_.addedFields.id if (!$memberid) { $memberid = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$member" -tenantid $TenantId).id From b4230b3e76fecee630aefa712b775a860801b7f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Sat, 31 May 2025 00:48:28 +0200 Subject: [PATCH 004/160] fix: fix all the casing of variables and methods --- .../Groups/Invoke-EditGroup.ps1 | 137 +++++++++--------- 1 file changed, 69 insertions(+), 68 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-EditGroup.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-EditGroup.ps1 index 37e88f23886f..aec47cff85ce 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-EditGroup.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-EditGroup.ps1 @@ -13,16 +13,16 @@ function Invoke-EditGroup { Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' $Results = [System.Collections.Generic.List[string]]@() - $userobj = $Request.body - $GroupType = $userobj.groupId.addedFields.groupType ? $userobj.groupId.addedFields.groupType : $userobj.groupType - $GroupName = $userobj.groupName ? $userobj.groupName : $userobj.groupId.addedFields.groupName + $UserObj = $Request.Body + $GroupType = $UserObj.groupId.addedFields.groupType ? $UserObj.groupId.addedFields.groupType : $UserObj.groupType + $GroupName = $UserObj.groupName ? $UserObj.groupName : $UserObj.groupId.addedFields.groupName #Write-Warning ($Request.Body | ConvertTo-Json -Depth 10) - $AddMembers = $userobj.AddMember - $userobj.groupId = $userobj.groupId.value ?? $userobj.groupId + $AddMembers = $UserObj.AddMember + $UserObj.groupId = $UserObj.groupId.value ?? $UserObj.groupId - $TenantId = $userobj.tenantid ?? $userobj.tenantFilter + $TenantId = $UserObj.tenantId ?? $UserObj.tenantFilter $MemberODataBindString = 'https://graph.microsoft.com/v1.0/directoryObjects/{0}' $BulkRequests = [System.Collections.Generic.List[object]]::new() @@ -35,13 +35,14 @@ function Invoke-EditGroup { try { # Add to group user action and edit group page sends in different formats, so we need to handle both $Member = $_.value ?? $_ - $memberid = $_.addedFields.id - if (!$memberid) { - $memberid = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$member" -tenantid $TenantId).id + $MemberID = $_.addedFields.id + if (!$MemberID) { + $MemberID = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$Member" -tenantid $TenantId).id } if ($GroupType -eq 'Distribution List' -or $GroupType -eq 'Mail-Enabled Security') { - $Params = @{ Identity = $userobj.groupid; Member = $member; BypassSecurityGroupManagerCheck = $true } + $Params = @{ Identity = $UserObj.groupId; Member = $Member; BypassSecurityGroupManagerCheck = $true } + # Write-Host ($UserObj | ConvertTo-Json -Depth 10) #Debugging line $ExoBulkRequests.Add(@{ CmdletInput = @{ CmdletName = 'Add-DistributionGroupMember' @@ -49,27 +50,27 @@ function Invoke-EditGroup { } }) $ExoLogs.Add(@{ - message = "Added member $member to $($GroupName) group" - target = $member + message = "Added member $Member to $($GroupName) group" + target = $Member }) } else { - $MemberIDs = $MemberODataBindString -f $memberid + $MemberIDs = $MemberODataBindString -f $MemberID $AddMemberBody = @{ 'members@odata.bind' = @($MemberIDs) } $BulkRequests.Add(@{ - id = "addMember-$member" + id = "addMember-$Member" method = 'PATCH' - url = "groups/$($userobj.groupid)" + url = "groups/$($UserObj.groupId)" body = $AddMemberBody headers = @{ 'Content-Type' = 'application/json' } }) $GraphLogs.Add(@{ - message = "Added member $member to $($GroupName) group" - id = "addMember-$member" + message = "Added member $Member to $($GroupName) group" + id = "addMember-$Member" }) } } catch { @@ -79,13 +80,13 @@ function Invoke-EditGroup { } - $AddContacts = $userobj.AddContact + $AddContacts = $UserObj.AddContact if ($AddContacts) { $AddContacts | ForEach-Object { try { - $member = $_ + $Member = $_ if ($GroupType -eq 'Distribution list' -or $GroupType -eq 'Mail-Enabled Security') { - $Params = @{ Identity = $userobj.groupid; Member = $member.value; BypassSecurityGroupManagerCheck = $true } + $Params = @{ Identity = $UserObj.groupId; Member = $Member.value; BypassSecurityGroupManagerCheck = $true } $ExoBulkRequests.Add(@{ CmdletInput = @{ CmdletName = 'Add-DistributionGroupMember' @@ -93,12 +94,12 @@ function Invoke-EditGroup { } }) $ExoLogs.Add(@{ - message = "Added contact $($member.label) to $($GroupName) group" - target = $member.value + message = "Added contact $($Member.label) to $($GroupName) group" + target = $Member.value }) } else { - Write-LogMessage -API $APINAME -tenant $TenantId -headers $Request.Headers -message 'You cannot add a Contact to a Security Group or a M365 Group' -Sev 'Error' - $null = $results.add('Error - You cannot add a contact to a Security Group or a M365 Group') + Write-LogMessage -API $APINAME -tenant $TenantId -headers $Headers -message 'You cannot add a Contact to a Security Group or a M365 Group' -Sev 'Error' + $null = $Results.Add('Error - You cannot add a contact to a Security Group or a M365 Group') } } catch { Write-Warning "Error in AddContacts: $($_.Exception.Message)" @@ -106,14 +107,14 @@ function Invoke-EditGroup { } } - $RemoveContact = $userobj.RemoveContact + $RemoveContact = $UserObj.RemoveContact try { if ($RemoveContact) { $RemoveContact | ForEach-Object { - $member = $_.value - $memberid = $_.addedFields.id + $Member = $_.value + $MemberID = $_.addedFields.id if ($GroupType -eq 'Distribution list' -or $GroupType -eq 'Mail-Enabled Security') { - $Params = @{ Identity = $userobj.groupid; Member = $memberid ; BypassSecurityGroupManagerCheck = $true } + $Params = @{ Identity = $UserObj.groupId; Member = $MemberID ; BypassSecurityGroupManagerCheck = $true } $ExoBulkRequests.Add(@{ CmdletInput = @{ CmdletName = 'Remove-DistributionGroupMember' @@ -121,12 +122,12 @@ function Invoke-EditGroup { } }) $ExoLogs.Add(@{ - message = "Removed contact $member from $($GroupName) group" - target = $memberid + message = "Removed contact $Member from $($GroupName) group" + target = $MemberID }) } else { - Write-LogMessage -API $APINAME -tenant $TenantId -headers $Request.Headers -message 'You cannot remove a contact from a Security Group' -Sev 'Error' - $null = $results.add('You cannot remove a contact from a Security Group') + Write-LogMessage -API $APINAME -tenant $TenantId -headers $Headers -message 'You cannot remove a contact from a Security Group' -Sev 'Error' + $null = $Results.Add('You cannot remove a contact from a Security Group') } } } @@ -134,14 +135,14 @@ function Invoke-EditGroup { Write-Warning "Error in RemoveContact: $($_.Exception.Message)" } - $RemoveMembers = $userobj.Removemember + $RemoveMembers = $UserObj.RemoveMember try { if ($RemoveMembers) { $RemoveMembers | ForEach-Object { - $member = $_.value - $memberid = $_.addedFields.id + $Member = $_.value + $MemberID = $_.addedFields.id if ($GroupType -eq 'Distribution list' -or $GroupType -eq 'Mail-Enabled Security') { - $Params = @{ Identity = $userobj.groupid; Member = $member ; BypassSecurityGroupManagerCheck = $true } + $Params = @{ Identity = $UserObj.groupId; Member = $Member ; BypassSecurityGroupManagerCheck = $true } $ExoBulkRequests.Add(@{ CmdletInput = @{ CmdletName = 'Remove-DistributionGroupMember' @@ -149,18 +150,18 @@ function Invoke-EditGroup { } }) $ExoLogs.Add(@{ - message = "Removed member $member from $($GroupName) group" - target = $member + message = "Removed member $Member from $($GroupName) group" + target = $Member }) } else { $BulkRequests.Add(@{ - id = "removeMember-$member" + id = "removeMember-$Member" method = 'DELETE' - url = "groups/$($userobj.groupid)/members/$memberid/`$ref" + url = "groups/$($UserObj.groupId)/members/$MemberID/`$ref" }) $GraphLogs.Add(@{ - message = "Removed member $member from $($GroupName) group" - id = "removeMember-$member" + message = "Removed member $Member from $($GroupName) group" + id = "removeMember-$Member" }) } } @@ -169,7 +170,7 @@ function Invoke-EditGroup { Write-Warning "Error in RemoveMembers: $($_.Exception.Message)" } - $AddOwners = $userobj.AddOwner + $AddOwners = $UserObj.AddOwner try { if ($AddOwners) { if ($GroupType -notin @('Distribution List', 'Mail-Enabled Security')) { @@ -180,7 +181,7 @@ function Invoke-EditGroup { $BulkRequests.Add(@{ id = "addOwner-$Owner" method = 'POST' - url = "groups/$($userobj.groupid)/owners/`$ref" + url = "groups/$($UserObj.groupId)/owners/`$ref" body = @{ '@odata.id' = $MemberODataBindString -f $ID } @@ -199,7 +200,7 @@ function Invoke-EditGroup { Write-Warning "Error in AddOwners: $($_.Exception.Message)" } - $RemoveOwners = $userobj.RemoveOwner + $RemoveOwners = $UserObj.RemoveOwner try { if ($RemoveOwners) { if ($GroupType -notin @('Distribution List', 'Mail-Enabled Security')) { @@ -208,7 +209,7 @@ function Invoke-EditGroup { $BulkRequests.Add(@{ id = "removeOwner-$ID" method = 'DELETE' - url = "groups/$($userobj.groupid)/owners/$ID/`$ref" + url = "groups/$($UserObj.groupId)/owners/$ID/`$ref" }) $GraphLogs.Add(@{ message = "Removed $($_.value) from $($GroupName) group" @@ -222,7 +223,7 @@ function Invoke-EditGroup { } if ($GroupType -in @( 'Distribution List', 'Mail-Enabled Security') -and ($AddOwners -or $RemoveOwners)) { - $CurrentOwners = New-ExoRequest -tenantid $TenantId -cmdlet 'Get-DistributionGroup' -cmdParams @{ Identity = $userobj.groupid } -UseSystemMailbox $true | Select-Object -ExpandProperty ManagedBy + $CurrentOwners = New-ExoRequest -tenantid $TenantId -cmdlet 'Get-DistributionGroup' -cmdParams @{ Identity = $UserObj.groupId } -UseSystemMailbox $true | Select-Object -ExpandProperty ManagedBy $NewManagedBy = [system.collections.generic.list[string]]::new() foreach ($CurrentOwner in $CurrentOwners) { @@ -230,7 +231,7 @@ function Invoke-EditGroup { $OwnerToRemove = $RemoveOwners | Where-Object { $_.addedFields.id -eq $CurrentOwner } $ExoLogs.Add(@{ message = "Removed owner $($OwnerToRemove.label) from $($GroupName) group" - target = $userobj.groupid + target = $UserObj.groupId }) continue } @@ -241,17 +242,17 @@ function Invoke-EditGroup { $NewManagedBy.Add($NewOwner.addedFields.id) $ExoLogs.Add(@{ message = "Added owner $($NewOwner.label) to $($GroupName) group" - target = $userobj.groupid + target = $UserObj.groupId }) } } $NewManagedBy = $NewManagedBy | Sort-Object -Unique - $params = @{ Identity = $userobj.groupid; ManagedBy = $NewManagedBy } + $Params = @{ Identity = $UserObj.groupId; ManagedBy = $NewManagedBy } $ExoBulkRequests.Add(@{ CmdletInput = @{ CmdletName = 'Set-DistributionGroup' - Parameters = $params + Parameters = $Params } }) } @@ -306,43 +307,43 @@ function Invoke-EditGroup { } } - if ($userobj.allowExternal -eq $true -and $GroupType -ne 'Security') { + if ($UserObj.allowExternal -eq $true -and $GroupType -ne 'Security') { try { - Set-CIPPGroupAuthentication -ID $userobj.mail -OnlyAllowInternal (!$userobj.allowExternal) -GroupType $GroupType -tenantFilter $TenantId -APIName $APINAME -Headers $Request.Headers - $body = $results.add("Allowed external senders to send to $($userobj.mail).") + Set-CIPPGroupAuthentication -ID $UserObj.mail -OnlyAllowInternal (!$UserObj.allowExternal) -GroupType $GroupType -tenantFilter $TenantId -APIName $APIName -Headers $Headers + $body = $Results.Add("Allowed external senders to send to $($UserObj.mail).") } catch { - $body = $results.add("Failed to allow external senders to send to $($userobj.mail).") - Write-LogMessage -headers $Request.Headers -API $APINAME -tenant $TenantId -message "Failed to allow external senders for $($userobj.mail). Error:$($_.Exception.Message)" -Sev 'Error' + $body = $Results.Add("Failed to allow external senders to send to $($UserObj.mail).") + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantId -message "Failed to allow external senders for $($UserObj.mail). Error:$($_.Exception.Message)" -Sev 'Error' } } - if ($userobj.sendCopies -eq $true) { + if ($UserObj.sendCopies -eq $true) { try { - $Params = @{ Identity = $userobj.Groupid; subscriptionEnabled = $true; AutoSubscribeNewMembers = $true } - New-ExoRequest -tenantid $TenantId -cmdlet 'Set-UnifiedGroup' -cmdParams $params -useSystemMailbox $true + $Params = @{ Identity = $UserObj.groupId; subscriptionEnabled = $true; AutoSubscribeNewMembers = $true } + New-ExoRequest -tenantid $TenantId -cmdlet 'Set-UnifiedGroup' -cmdParams $Params -useSystemMailbox $true - $MemberParams = @{ Identity = $userobj.Groupid; LinkType = 'members' } - $Members = New-ExoRequest -tenantid $TenantId -cmdlet 'Get-UnifiedGrouplinks' -cmdParams $MemberParams + $MemberParams = @{ Identity = $UserObj.groupId; LinkType = 'members' } + $Members = New-ExoRequest -tenantid $TenantId -cmdlet 'Get-UnifiedGroupLinks' -cmdParams $MemberParams $MemberSmtpAddresses = $Members | ForEach-Object { $_.PrimarySmtpAddress } if ($MemberSmtpAddresses) { - $subscriberParams = @{ Identity = $userobj.Groupid; LinkType = 'subscribers'; Links = @($MemberSmtpAddresses | Where-Object { $_ }) } - New-ExoRequest -tenantid $TenantId -cmdlet 'Add-UnifiedGrouplinks' -cmdParams $subscriberParams -Anchor $userobj.mail + $subscriberParams = @{ Identity = $UserObj.groupId; LinkType = 'subscribers'; Links = @($MemberSmtpAddresses | Where-Object { $_ }) } + New-ExoRequest -tenantid $TenantId -cmdlet 'Add-UnifiedGroupLinks' -cmdParams $subscriberParams -Anchor $UserObj.mail } - $body = $results.add("Send Copies of team emails and events to team members inboxes for $($userobj.mail) enabled.") - Write-LogMessage -headers $Request.Headers -API $APINAME -tenant $TenantId -message "Send Copies of team emails and events to team members inboxes for $($userobj.mail) enabled." -Sev 'Info' + $body = $Results.Add("Send Copies of team emails and events to team members inboxes for $($UserObj.mail) enabled.") + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantId -message "Send Copies of team emails and events to team members inboxes for $($UserObj.mail) enabled." -Sev 'Info' } catch { Write-Warning "Error in SendCopies: $($_.Exception.Message) - $($_.InvocationInfo.ScriptLineNumber)" Write-Warning ($_.InvocationInfo.PositionMessage) - $body = $results.add("Failed to Send Copies of team emails and events to team members inboxes for $($userobj.mail).") - Write-LogMessage -headers $Request.Headers -API $APINAME -tenant $TenantId -message "Failed to Send Copies of team emails and events to team members inboxes for $($userobj.mail). Error:$($_.Exception.Message)" -Sev 'Error' + $body = $Results.Add("Failed to Send Copies of team emails and events to team members inboxes for $($UserObj.mail).") + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantId -message "Failed to Send Copies of team emails and events to team members inboxes for $($UserObj.mail). Error:$($_.Exception.Message)" -Sev 'Error' } } - $body = @{'Results' = @($results) } + $body = @{'Results' = @($Results) } # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::OK From 48f38ee85c30835d18392cf1e5bd6520b2398864 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Sat, 31 May 2025 00:53:57 +0200 Subject: [PATCH 005/160] fix: copy groups now uses ExternalDirectoryObjectID to prevent Microsoft.Exchange.Configuration.Tasks.ManagementObjectAmbiguousException error --- Modules/CIPPCore/Public/Set-CIPPCopyGroupMembers.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Set-CIPPCopyGroupMembers.ps1 b/Modules/CIPPCore/Public/Set-CIPPCopyGroupMembers.ps1 index 9564d818f6d5..86a482d20412 100644 --- a/Modules/CIPPCore/Public/Set-CIPPCopyGroupMembers.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPCopyGroupMembers.ps1 @@ -31,7 +31,7 @@ function Set-CIPPCopyGroupMembers { $CurrentMemberships = ($Results | Where-Object { $_.id -eq 'UserMembership' }).body.value $CopyFromMemberships = ($Results | Where-Object { $_.id -eq 'CopyFromMembership' }).body.value - Write-Information ($Results | ConvertTo-Json -Depth 10) + # Write-Information ($Results | ConvertTo-Json -Depth 10) # For debugging $ODataBind = 'https://graph.microsoft.com/v1.0/directoryObjects/{0}' -f $User.id $AddMemberBody = @{ @@ -45,7 +45,7 @@ function Set-CIPPCopyGroupMembers { foreach ($MailGroup in $Memberships) { try { if ($PSCmdlet.ShouldProcess($MailGroup.displayName, "Add $UserId to group")) { - if ($MailGroup.MailEnabled -and $Mailgroup.ResourceProvisioningOptions -notcontains 'Team' -and $MailGroup.groupTypes -notcontains 'Unified') { + if ($MailGroup.MailEnabled -and $MailGroup.ResourceProvisioningOptions -notcontains 'Team' -and $MailGroup.groupTypes -notcontains 'Unified') { $Params = @{ Identity = $MailGroup.id; Member = $UserId; BypassSecurityGroupManagerCheck = $true } try { $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Add-DistributionGroupMember' -cmdParams $params -UseSystemMailbox $true From b823d3591e88e9d6097d110cac6d176750688703 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Sat, 31 May 2025 00:56:31 +0200 Subject: [PATCH 006/160] readability --- Modules/CIPPCore/Public/Set-CIPPCopyGroupMembers.ps1 | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Set-CIPPCopyGroupMembers.ps1 b/Modules/CIPPCore/Public/Set-CIPPCopyGroupMembers.ps1 index 86a482d20412..705b6231045b 100644 --- a/Modules/CIPPCore/Public/Set-CIPPCopyGroupMembers.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPCopyGroupMembers.ps1 @@ -40,7 +40,11 @@ function Set-CIPPCopyGroupMembers { $Success = [System.Collections.Generic.List[object]]::new() $Errors = [System.Collections.Generic.List[object]]::new() - $Memberships = $CopyFromMemberships | Where-Object { $_.'@odata.type' -eq '#microsoft.graph.group' -and $_.groupTypes -notcontains 'DynamicMembership' -and $_.onPremisesSyncEnabled -ne $true -and $_.visibility -ne 'Public' -and $CurrentMemberships.id -notcontains $_.id } + $Memberships = $CopyFromMemberships | Where-Object { $_.'@odata.type' -eq '#microsoft.graph.group' -and + $_.groupTypes -notcontains 'DynamicMembership' -and + $_.onPremisesSyncEnabled -ne $true -and + $_.visibility -ne 'Public' -and + $CurrentMemberships.id -notcontains $_.id } $ScheduleExchangeGroupTask = $false foreach ($MailGroup in $Memberships) { try { From ecbaa3d6720c3c2e932cf19df259930683431a2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Sat, 31 May 2025 00:58:15 +0200 Subject: [PATCH 007/160] fix: correct variable name from $APINAME to $APIName in logging messages --- .../Administration/Groups/Invoke-EditGroup.ps1 | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-EditGroup.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-EditGroup.ps1 index aec47cff85ce..a6e660ad0320 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-EditGroup.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-EditGroup.ps1 @@ -98,7 +98,7 @@ function Invoke-EditGroup { target = $Member.value }) } else { - Write-LogMessage -API $APINAME -tenant $TenantId -headers $Headers -message 'You cannot add a Contact to a Security Group or a M365 Group' -Sev 'Error' + Write-LogMessage -API $APIName -tenant $TenantId -headers $Headers -message 'You cannot add a Contact to a Security Group or a M365 Group' -Sev 'Error' $null = $Results.Add('Error - You cannot add a contact to a Security Group or a M365 Group') } } catch { @@ -126,7 +126,7 @@ function Invoke-EditGroup { target = $MemberID }) } else { - Write-LogMessage -API $APINAME -tenant $TenantId -headers $Headers -message 'You cannot remove a contact from a Security Group' -Sev 'Error' + Write-LogMessage -API $APIName-tenant $TenantId -headers $Headers -message 'You cannot remove a contact from a Security Group' -Sev 'Error' $null = $Results.Add('You cannot remove a contact from a Security Group') } } @@ -276,7 +276,7 @@ function Invoke-EditGroup { $Sev = 'Info' $Results.Add("Success - $Message") } - Write-LogMessage -headers $Request.Headers -API $APINAME -tenant $TenantId -message $Message -Sev $Sev + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantId -message $Message -Sev $Sev } } @@ -293,7 +293,7 @@ function Invoke-EditGroup { foreach ($ExoError in $LastError.error) { $Sev = 'Error' $Results.Add("Error - $ExoError") - Write-LogMessage -headers $Request.Headers -API $APINAME -tenant $TenantId -message $Message -Sev $Sev + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantId -message $Message -Sev $Sev } foreach ($ExoLog in $ExoLogs) { @@ -302,7 +302,7 @@ function Invoke-EditGroup { $Message = $ExoLog.message $Sev = 'Info' $Results.Add("Success - $Message") - Write-LogMessage -headers $Request.Headers -API $APINAME -tenant $TenantId -message $Message -Sev $Sev + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantId -message $Message -Sev $Sev } } } From 8a09b26befdd40ab60d78480d053352cb17211a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Sat, 31 May 2025 01:10:59 +0200 Subject: [PATCH 008/160] fix: casing and change very odd error message --- .../Administration/Users/Invoke-EditUser.ps1 | 53 ++++++++++--------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditUser.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditUser.ps1 index 33d5b08ceea5..08969005537f 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditUser.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditUser.ps1 @@ -12,7 +12,7 @@ function Invoke-EditUser { $APIName = $Request.Params.CIPPEndpoint $Headers = $Request.Headers - Write-LogMessage -headers $Headers -API $ApiName -message 'Accessed this API' -Sev 'Debug' + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' $UserObj = $Request.Body if ([string]::IsNullOrWhiteSpace($UserObj.id)) { @@ -70,16 +70,16 @@ function Invoke-EditUser { $bodyToShip = ConvertTo-Json -Depth 10 -InputObject $BodyToship -Compress $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/users/$($UserObj.id)" -tenantid $UserObj.tenantFilter -type PATCH -body $BodyToship -verbose $null = $Results.Add( 'Success. The user has been edited.' ) - Write-LogMessage -API $ApiName -tenant ($UserObj.tenantFilter) -headers $Headers -message "Edited user $($UserObj.DisplayName) with id $($UserObj.id)" -Sev Info + Write-LogMessage -API $APIName -tenant ($UserObj.tenantFilter) -headers $Headers -message "Edited user $($UserObj.DisplayName) with id $($UserObj.id)" -Sev Info if ($UserObj.password) { $passwordProfile = [pscustomobject]@{'passwordProfile' = @{ 'password' = $UserObj.password; 'forceChangePasswordNextSignIn' = [boolean]$UserObj.MustChangePass } } | ConvertTo-Json $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/users/$($UserObj.id)" -tenantid $UserObj.tenantFilter -type PATCH -body $PasswordProfile -verbose $null = $Results.Add("Success. The password has been set to $($UserObj.password)") - Write-LogMessage -API $ApiName -tenant ($UserObj.tenantFilter) -headers $Headers -message "Reset $($UserObj.DisplayName)'s Password" -Sev Info + Write-LogMessage -API $APIName -tenant ($UserObj.tenantFilter) -headers $Headers -message "Reset $($UserObj.DisplayName)'s Password" -Sev Info } } catch { $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -API $ApiName -tenant ($UserObj.tenantFilter) -headers $Headers -message "User edit API failed. $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage + Write-LogMessage -API $APIName -tenant ($UserObj.tenantFilter) -headers $Headers -message "User edit API failed. $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage $null = $Results.Add( "Failed to edit user. $($ErrorMessage.NormalizedError)") } @@ -115,16 +115,16 @@ function Invoke-EditUser { #if the list of skuIds in $CurrentLicenses.assignedLicenses is EXACTLY the same as $licenses, we don't need to do anything, but the order in both can be different. if (($CurrentLicenses.assignedLicenses.skuId -join ',') -eq ($licenses -join ',') -and $UserObj.removeLicenses -eq $false) { Write-Host "$($CurrentLicenses.assignedLicenses.skuId -join ',') $(($licenses -join ','))" - $null = $results.Add( 'Success. User license is already correct.' ) + $null = $Results.Add( 'Success. User license is already correct.' ) } else { if ($UserObj.removeLicenses) { $licResults = Set-CIPPUserLicense -UserId $UserObj.id -TenantFilter $UserObj.tenantFilter -RemoveLicenses $CurrentLicenses.assignedLicenses.skuId -Headers $Headers - $null = $results.Add($licResults) + $null = $Results.Add($licResults) } else { #Remove all objects from $CurrentLicenses.assignedLicenses.skuId that are in $licenses $RemoveLicenses = $CurrentLicenses.assignedLicenses.skuId | Where-Object { $_ -notin $licenses } $licResults = Set-CIPPUserLicense -UserId $UserObj.id -TenantFilter $UserObj.tenantFilter -RemoveLicenses $RemoveLicenses -AddLicenses $licenses -Headers $headers - $null = $results.Add($licResults) + $null = $Results.Add($licResults) } } @@ -133,8 +133,8 @@ function Invoke-EditUser { } catch { $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -API $ApiName -tenant ($UserObj.tenantFilter) -headers $Headers -message "License assign API failed. $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage - $null = $results.Add( "We've failed to assign the license. $($ErrorMessage.NormalizedError)") + Write-LogMessage -API $APIName -tenant ($UserObj.tenantFilter) -headers $Headers -message "License assign API failed. $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage + $null = $Results.Add( "We've failed to assign the license. $($ErrorMessage.NormalizedError)") Write-Warning "License assign API failed. $($_.Exception.Message)" Write-Information $_.InvocationInfo.PositionMessage } @@ -147,19 +147,20 @@ function Invoke-EditUser { $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/users/$($UserObj.id)" -tenantid $UserObj.tenantFilter -type 'patch' -body "{`"mail`": `"$Alias`"}" -Verbose } $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/users/$($UserObj.id)" -tenantid $UserObj.tenantFilter -type 'patch' -body "{`"mail`": `"$UserPrincipalName`"}" -Verbose - Write-LogMessage -API $ApiName -tenant ($UserObj.tenantFilter) -headers $Headers -message "Added Aliases to $($UserObj.DisplayName)" -Sev Info - $null = $results.Add( 'Success. added aliases to user.') + Write-LogMessage -API $APIName -tenant ($UserObj.tenantFilter) -headers $Headers -message "Added Aliases to $($UserObj.DisplayName)" -Sev Info + $null = $Results.Add( 'Success. Added aliases to user.') } } catch { $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -API $ApiName -tenant ($UserObj.tenantFilter) -headers $Headers -message "Alias API failed. $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage - $null = $results.Add( "Successfully edited user. The password is $password. We've failed to create the Aliases: $($ErrorMessage.NormalizedError)") + $Message = "Failed to add aliases to user $($UserObj.DisplayName). Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -API $APIName -tenant ($UserObj.tenantFilter) -headers $Headers -message $Message -Sev Error -LogData $ErrorMessage + $null = $Results.Add($Message) } if ($Request.Body.CopyFrom.value) { $CopyFrom = Set-CIPPCopyGroupMembers -Headers $Headers -CopyFromId $Request.Body.CopyFrom.value -UserID $UserPrincipalName -TenantFilter $UserObj.tenantFilter - $null = $results.AddRange(@($CopyFrom)) + $null = $Results.AddRange(@($CopyFrom)) } if ($AddToGroups) { @@ -183,13 +184,13 @@ function Invoke-EditUser { $UserBodyJSON = ConvertTo-Json -Compress -Depth 10 -InputObject $UserBody $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/groups/$GroupID/members/`$ref" -tenantid $UserObj.tenantFilter -type POST -body $UserBodyJSON -Verbose } - Write-LogMessage -headers $Headers -API $ApiName -tenant $UserObj.tenantFilter -message "Added $($UserObj.DisplayName) to $GroupName group" -Sev Info - $null = $results.Add("Success. $($UserObj.DisplayName) has been added to $GroupName") + Write-LogMessage -headers $Headers -API $APIName -tenant $UserObj.tenantFilter -message "Added $($UserObj.DisplayName) to $GroupName group" -Sev Info + $null = $Results.Add("Success. $($UserObj.DisplayName) has been added to $GroupName") } catch { $ErrorMessage = Get-CippException -Exception $_ $Message = "Failed to add member $($UserObj.DisplayName) to $GroupName. Error: $($ErrorMessage.NormalizedError)" - Write-LogMessage -headers $Headers -API $ApiName -tenant $UserObj.tenantFilter -message $Message -Sev Error -LogData $ErrorMessage - $null = $results.Add($Message) + Write-LogMessage -headers $Headers -API $APIName -tenant $UserObj.tenantFilter -message $Message -Sev Error -LogData $ErrorMessage + $null = $Results.Add($Message) } } } @@ -211,13 +212,13 @@ function Invoke-EditUser { Write-Host 'Removing From group via Graph' $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/groups/$GroupID/members/$($UserObj.id)/`$ref" -tenantid $UserObj.tenantFilter -type DELETE } - Write-LogMessage -headers $Headers -API $ApiName -tenant $UserObj.tenantFilter -message "Removed $($UserObj.DisplayName) from $GroupName group" -Sev Info - $null = $results.Add("Success. $($UserObj.DisplayName) has been removed from $GroupName") + Write-LogMessage -headers $Headers -API $APIName -tenant $UserObj.tenantFilter -message "Removed $($UserObj.DisplayName) from $GroupName group" -Sev Info + $null = $Results.Add("Success. $($UserObj.DisplayName) has been removed from $GroupName") } catch { $ErrorMessage = Get-CippException -Exception $_ $Message = "Failed to remove member $($UserObj.DisplayName) from $GroupName. Error: $($ErrorMessage.NormalizedError)" - Write-LogMessage -headers $Headers -API $ApiName -tenant $UserObj.tenantFilter -message $Message -Sev Error -LogData $ErrorMessage - $null = $results.Add($Message) + Write-LogMessage -headers $Headers -API $APIName -tenant $UserObj.tenantFilter -message $Message -Sev Error -LogData $ErrorMessage + $null = $Results.Add($Message) } } } @@ -226,16 +227,16 @@ function Invoke-EditUser { $ManagerBody = [PSCustomObject]@{'@odata.id' = "https://graph.microsoft.com/beta/users/$($Request.body.setManager.value)" } $ManagerBodyJSON = ConvertTo-Json -Compress -Depth 10 -InputObject $ManagerBody $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/users/$($UserObj.id)/manager/`$ref" -tenantid $UserObj.tenantFilter -type PUT -body $ManagerBodyJSON -Verbose - Write-LogMessage -headers $Headers -API $ApiName -tenant $UserObj.tenantFilter -message "Set $($UserObj.DisplayName)'s manager to $($Request.body.setManager.label)" -Sev Info - $null = $results.Add("Success. Set $($UserObj.DisplayName)'s manager to $($Request.body.setManager.label)") + Write-LogMessage -headers $Headers -API $APIName -tenant $UserObj.tenantFilter -message "Set $($UserObj.DisplayName)'s manager to $($Request.body.setManager.label)" -Sev Info + $null = $Results.Add("Success. Set $($UserObj.DisplayName)'s manager to $($Request.body.setManager.label)") } if ($Request.body.setSponsor.value) { $SponsorBody = [PSCustomObject]@{'@odata.id' = "https://graph.microsoft.com/beta/users/$($Request.body.setSponsor.value)" } $SponsorBodyJSON = ConvertTo-Json -Compress -Depth 10 -InputObject $SponsorBody $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/users/$($UserObj.id)/sponsors/`$ref" -tenantid $UserObj.tenantFilter -type POST -body $SponsorBodyJSON -Verbose - Write-LogMessage -headers $Headers -API $ApiName -tenant $UserObj.tenantFilter -message "Set $($UserObj.DisplayName)'s sponsor to $($Request.body.setSponsor.label)" -Sev Info - $null = $results.Add("Success. Set $($UserObj.DisplayName)'s sponsor to $($Request.body.setSponsor.label)") + Write-LogMessage -headers $Headers -API $APIName -tenant $UserObj.tenantFilter -message "Set $($UserObj.DisplayName)'s sponsor to $($Request.body.setSponsor.label)" -Sev Info + $null = $Results.Add("Success. Set $($UserObj.DisplayName)'s sponsor to $($Request.body.setSponsor.label)") } $body = @{'Results' = @($results) } From 98e5e3746701fcc21b093fd2c57c2711d3511015 Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Sat, 31 May 2025 12:14:57 +0800 Subject: [PATCH 009/160] New options for creating and editing contacts --- .../Administration/Invoke-AddContact.ps1 | 53 ++++- .../Administration/Invoke-EditContact.ps1 | 37 ++-- .../Administration/Invoke-ListContacts.ps1 | 190 ++++++++++++++---- 3 files changed, 213 insertions(+), 67 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-AddContact.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-AddContact.ps1 index 1e7332d359b9..2ae5bd7e34df 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-AddContact.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-AddContact.ps1 @@ -18,25 +18,57 @@ Function Invoke-AddContact { $TenantId = $ContactObject.tenantid try { - + # Prepare the body for New-MailContact cmdlet $BodyToship = [pscustomobject] @{ displayName = $ContactObject.displayName name = $ContactObject.displayName ExternalEmailAddress = $ContactObject.email FirstName = $ContactObject.firstName LastName = $ContactObject.lastName - } - # Create the contact + + # Create the mail contact first $NewContact = New-ExoRequest -tenantid $TenantId -cmdlet 'New-MailContact' -cmdParams $BodyToship -UseSystemMailbox $true - $null = New-ExoRequest -tenantid $TenantId -cmdlet 'Set-MailContact' -cmdParams @{Identity = $NewContact.id; HiddenFromAddressListsEnabled = [boolean]$ContactObject.hidefromGAL } -UseSystemMailbox $true + + # Prepare the body for Set-Contact cmdlet to add additional details + $SetContactParams = [pscustomobject] @{ + Identity = $NewContact.id + } + + # Add optional fields if they exist + if ($ContactObject.Title) { $SetContactParams | Add-Member -MemberType NoteProperty -Name 'Title' -Value $ContactObject.Title } + if ($ContactObject.Company) { $SetContactParams | Add-Member -MemberType NoteProperty -Name 'Company' -Value $ContactObject.Company } + if ($ContactObject.StreetAddress) { $SetContactParams | Add-Member -MemberType NoteProperty -Name 'StreetAddress' -Value $ContactObject.StreetAddress } + if ($ContactObject.City) { $SetContactParams | Add-Member -MemberType NoteProperty -Name 'City' -Value $ContactObject.City } + if ($ContactObject.State) { $SetContactParams | Add-Member -MemberType NoteProperty -Name 'StateOrProvince' -Value $ContactObject.State } + if ($ContactObject.PostalCode) { $SetContactParams | Add-Member -MemberType NoteProperty -Name 'PostalCode' -Value $ContactObject.PostalCode } + if ($ContactObject.CountryOrRegion) { $SetContactParams | Add-Member -MemberType NoteProperty -Name 'CountryOrRegion' -Value $ContactObject.CountryOrRegion } + if ($ContactObject.phone) { $SetContactParams | Add-Member -MemberType NoteProperty -Name 'Phone' -Value $ContactObject.phone } + if ($ContactObject.mobilePhone) { $SetContactParams | Add-Member -MemberType NoteProperty -Name 'MobilePhone' -Value $ContactObject.mobilePhone } + if ($ContactObject.website) { $SetContactParams | Add-Member -MemberType NoteProperty -Name 'WebPage' -Value $ContactObject.website } + + # Update the contact with additional details + $null = New-ExoRequest -tenantid $TenantId -cmdlet 'Set-Contact' -cmdParams $SetContactParams -UseSystemMailbox $true + + # Set mail contact specific properties + $MailContactParams = @{ + Identity = $NewContact.id + HiddenFromAddressListsEnabled = [boolean]$ContactObject.hidefromGAL + } + + # Add MailTip if provided + if ($ContactObject.mailTip) { + $MailContactParams.MailTip = $ContactObject.mailTip + } + + $null = New-ExoRequest -tenantid $TenantId -cmdlet 'Set-MailContact' -cmdParams $MailContactParams -UseSystemMailbox $true # Log the result - $Result = "Created contact $($ContactObject.displayName) with email address $($ContactObject.email)" + $Result = "Successfully created contact $($ContactObject.displayName) with email address $($ContactObject.email)" Write-LogMessage -headers $Headers -API $APIName -tenant $TenantId -message $Result -Sev 'Info' $StatusCode = [HttpStatusCode]::OK - - } catch { + } + catch { $ErrorMessage = Get-CippException -Exception $_ $Result = "Failed to create contact. $($ErrorMessage.NormalizedError)" Write-LogMessage -headers $Headers -API $APIName -tenant $TenantId -message $Result -Sev 'Error' -LogData $ErrorMessage @@ -46,8 +78,7 @@ Function Invoke-AddContact { # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ - StatusCode = $StatusCode - Body = @{Results = $Result } - }) - + StatusCode = $StatusCode + Body = @{Results = $Result } + }) } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-EditContact.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-EditContact.ps1 index d46143ce376c..3446b866b074 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-EditContact.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-EditContact.ps1 @@ -15,13 +15,11 @@ Function Invoke-EditContact { Write-LogMessage -Headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' $TenantID = $Request.Body.tenantID + try { # Extract contact information from the request body $contactInfo = $Request.Body - # Log the received contact object - Write-Host "Received contact object: $($contactInfo | ConvertTo-Json)" - # Prepare the body for the Set-Contact cmdlet $bodyForSetContact = [pscustomobject] @{ 'Identity' = $contactInfo.ContactID @@ -33,30 +31,45 @@ Function Invoke-EditContact { 'StreetAddress' = $contactInfo.StreetAddress 'PostalCode' = $contactInfo.PostalCode 'City' = $contactInfo.City + 'StateOrProvince' = $contactInfo.State 'CountryOrRegion' = $contactInfo.CountryOrRegion 'Company' = $contactInfo.Company - 'mobilePhone' = $contactInfo.mobilePhone - 'phone' = $contactInfo.phone + 'MobilePhone' = $contactInfo.mobilePhone + 'Phone' = $contactInfo.phone + 'WebPage' = $contactInfo.website } # Call the Set-Contact cmdlet to update the contact $null = New-ExoRequest -tenantid $TenantID -cmdlet 'Set-Contact' -cmdParams $bodyForSetContact -UseSystemMailbox $true - $null = New-ExoRequest -tenantid $TenantID -cmdlet 'Set-MailContact' -cmdParams @{Identity = $contactInfo.ContactID; HiddenFromAddressListsEnabled = [System.Convert]::ToBoolean($contactInfo.hidefromGAL) } -UseSystemMailbox $true + + # Prepare mail contact specific parameters + $MailContactParams = @{ + Identity = $contactInfo.ContactID + HiddenFromAddressListsEnabled = [System.Convert]::ToBoolean($contactInfo.hidefromGAL) + } + + # Add MailTip if provided + if ($contactInfo.mailTip) { + $MailContactParams.MailTip = $contactInfo.mailTip + } + + # Update mail contact properties + $null = New-ExoRequest -tenantid $TenantID -cmdlet 'Set-MailContact' -cmdParams $MailContactParams -UseSystemMailbox $true + $Results = "Successfully edited contact $($contactInfo.DisplayName)" Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantID -message $Results -Sev Info $StatusCode = [HttpStatusCode]::OK - - } catch { + } + catch { $ErrorMessage = Get-CippException -Exception $_ $Results = "Failed to edit contact. $($ErrorMessage.NormalizedError)" Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantID -message $Results -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 = @{Results = $Results } - }) + StatusCode = $StatusCode + Body = @{Results = $Results } + }) } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListContacts.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListContacts.ps1 index 6fb5562635a4..34355e55a261 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListContacts.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListContacts.ps1 @@ -1,4 +1,6 @@ using namespace System.Net +using namespace System.Collections.Generic +using namespace System.Text.RegularExpressions Function Invoke-ListContacts { <# @@ -10,62 +12,162 @@ Function Invoke-ListContacts { [CmdletBinding()] param($Request, $TriggerMetadata) - - # Define fields to retrieve - $selectList = @( - 'id', - 'companyName', - 'displayName', - 'mail', - 'onPremisesSyncEnabled', - 'editURL', - 'givenName', - 'jobTitle', - 'surname', - 'addresses', - 'phones' - ) - # Get query parameters $TenantFilter = $Request.Query.tenantFilter $ContactID = $Request.Query.id - # Validate required parameters + # Early validation and exit if (-not $TenantFilter) { - $StatusCode = [HttpStatusCode]::BadRequest - $GraphRequest = 'tenantFilter is required' - Write-Host 'Error: Missing tenantFilter parameter' - } else { - try { - # Construct Graph API URI based on whether an ID is provided - $graphUri = if ([string]::IsNullOrWhiteSpace($ContactID) -eq $false) { - "https://graph.microsoft.com/beta/contacts/$($ContactID)?`$select=$($selectList -join ',')" - } else { - "https://graph.microsoft.com/beta/contacts?`$top=999&`$select=$($selectList -join ',')" + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = 'tenantFilter is required' + }) + return + } + + # Pre-compiled regex for MailTip cleaning + $script:HtmlTagRegex ??= [regex]::new('<[^>]+>', [RegexOptions]::Compiled) + $script:LineBreakRegex ??= [regex]::new('\\n|\r\n|\r', [RegexOptions]::Compiled) + $script:SmtpPrefixRegex ??= [regex]::new('^SMTP:', [RegexOptions]::Compiled -bor [RegexOptions]::IgnoreCase) + + function ConvertTo-ContactObject { + param($Contact, $MailContact) + + # Early exit if essential data missing + if (!$Contact.Id) { return $null } + + $mailAddress = if ($MailContact.ExternalEmailAddress) { + $script:SmtpPrefixRegex.Replace($MailContact.ExternalEmailAddress, [string]::Empty, 1) + } else { $null } + + $cleanMailTip = if ($MailContact.MailTip -and $MailContact.MailTip.Length -gt 0) { + $cleaned = $script:HtmlTagRegex.Replace($MailContact.MailTip, [string]::Empty) + $cleaned = $script:LineBreakRegex.Replace($cleaned, "`n") + $cleaned.Trim() + } else { $null } + + $phoneCapacity = 0 + if ($Contact.Phone) { $phoneCapacity++ } + if ($Contact.MobilePhone) { $phoneCapacity++ } + + $phones = if ($phoneCapacity -gt 0) { + $phoneList = [List[hashtable]]::new($phoneCapacity) + if ($Contact.Phone) { + $phoneList.Add(@{ type = "business"; number = $Contact.Phone }) } + if ($Contact.MobilePhone) { + $phoneList.Add(@{ type = "mobile"; number = $Contact.MobilePhone }) + } + $phoneList.ToArray() + } else { @() } - # Make the Graph API request - $GraphRequest = New-GraphGetRequest -uri $graphUri -tenantid $TenantFilter + return @{ + id = $Contact.Id + displayName = $Contact.DisplayName + givenName = $Contact.FirstName + surname = $Contact.LastName + mail = $mailAddress + companyName = $Contact.Company + jobTitle = $Contact.Title + website = $Contact.WebPage + notes = $Contact.Notes + hidefromGAL = $MailContact.HiddenFromAddressListsEnabled + mailTip = $cleanMailTip + onPremisesSyncEnabled = $Contact.IsDirSynced + addresses = @(@{ + street = $Contact.StreetAddress + city = $Contact.City + state = $Contact.StateOrProvince + countryOrRegion = $Contact.CountryOrRegion + postalCode = $Contact.PostalCode + }) + phones = $phones + } + } + + try { + if (![string]::IsNullOrWhiteSpace($ContactID)) { + # Single contact request + Write-Host "Getting specific contact: $ContactID" - if ([string]::IsNullOrWhiteSpace($ContactID) -eq $false) { - $HiddenFromGAL = New-EXORequest -tenantid $TenantFilter -cmdlet 'Get-Recipient' -cmdParams @{RecipientTypeDetails = 'MailContact' } -Select 'HiddenFromAddressListsEnabled,ExternalDirectoryObjectId' | Where-Object { $_.ExternalDirectoryObjectId -eq $ContactID } - $GraphRequest | Add-Member -NotePropertyName 'hidefromGAL' -NotePropertyValue $HiddenFromGAL.HiddenFromAddressListsEnabled + $Contact = New-EXORequest -tenantid $TenantFilter -cmdlet 'Get-Contact' -cmdParams @{ + Identity = $ContactID } - # Ensure single result when ID is provided - if ($ContactID -and $GraphRequest -is [array]) { - $GraphRequest = $GraphRequest | Select-Object -First 1 + + $MailContact = New-EXORequest -tenantid $TenantFilter -cmdlet 'Get-MailContact' -cmdParams @{ + Identity = $ContactID + } + + if (!$Contact -or !$MailContact) { + throw "Contact not found or insufficient permissions" + } + + $ContactResponse = ConvertTo-ContactObject -Contact $Contact -MailContact $MailContact + + } else { + # Get all contacts + Write-Host "Getting all contacts" + + $Contacts = New-EXORequest -tenantid $TenantFilter -cmdlet 'Get-Contact' -cmdParams @{ + RecipientTypeDetails = 'MailContact' + ResultSize = 'Unlimited' + } + + # Exit if no contacts + if (!$Contacts -or $Contacts.Count -eq 0) { + $ContactResponse = @() + } else { + # Filter contacts with missing IDs + $ValidContacts = $Contacts.Where({$_.Id -and $_.Identity}) + + if ($ValidContacts.Count -eq 0) { + $ContactResponse = @() + } else { + $ContactIdentities = $ValidContacts.Identity + $MailContacts = New-EXORequest -tenantid $TenantFilter -cmdlet 'Get-MailContact' -cmdParams @{ + ResultSize = 'Unlimited' + } | Where-Object { $_.Identity -in $ContactIdentities } + + # Build dictionary + $MailContactLookup = [Dictionary[string, object]]::new( + $MailContacts.Count, + [StringComparer]::OrdinalIgnoreCase + ) + + foreach ($mc in $MailContacts) { + if ($mc.Identity) { + $MailContactLookup[$mc.Identity] = $mc + } + } + + $FormattedContacts = [List[object]]::new($ValidContacts.Count) + + # Process contacts + foreach ($Contact in $ValidContacts) { + if ($MailContactLookup.ContainsKey($Contact.Identity)) { + $ContactObj = ConvertTo-ContactObject -Contact $Contact -MailContact $MailContactLookup[$Contact.Identity] + if ($ContactObj) { + $FormattedContacts.Add($ContactObj) + } + } + } + + $ContactResponse = $FormattedContacts.ToArray() + } } - $StatusCode = [HttpStatusCode]::OK - } catch { - $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - $StatusCode = [HttpStatusCode]::InternalServerError - $GraphRequest = $ErrorMessage } + + $StatusCode = [HttpStatusCode]::OK + + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + $StatusCode = [HttpStatusCode]::InternalServerError + $ContactResponse = $ErrorMessage + Write-Host "Error in ListContacts: $ErrorMessage" } - # Return response Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ - StatusCode = $StatusCode - Body = @($GraphRequest | Where-Object { $null -ne $_.id }) - }) + StatusCode = $StatusCode + Body = $ContactResponse + }) } From 1d278ca0a4a5350d44ee85660b0a6ffad3aa4537 Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Sat, 31 May 2025 12:44:48 +0800 Subject: [PATCH 010/160] Consistency and optimisation --- .../Administration/Invoke-AddContact.ps1 | 49 ++++++++++++------- .../Administration/Invoke-EditContact.ps1 | 39 +++++++++++---- 2 files changed, 59 insertions(+), 29 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-AddContact.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-AddContact.ps1 index 2ae5bd7e34df..7cec494d892c 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-AddContact.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-AddContact.ps1 @@ -19,7 +19,7 @@ Function Invoke-AddContact { try { # Prepare the body for New-MailContact cmdlet - $BodyToship = [pscustomobject] @{ + $BodyToship = @{ displayName = $ContactObject.displayName name = $ContactObject.displayName ExternalEmailAddress = $ContactObject.email @@ -30,34 +30,45 @@ Function Invoke-AddContact { # Create the mail contact first $NewContact = New-ExoRequest -tenantid $TenantId -cmdlet 'New-MailContact' -cmdParams $BodyToship -UseSystemMailbox $true - # Prepare the body for Set-Contact cmdlet to add additional details - $SetContactParams = [pscustomobject] @{ + # Build SetContactParams efficiently with only provided values + $SetContactParams = @{ Identity = $NewContact.id } - # Add optional fields if they exist - if ($ContactObject.Title) { $SetContactParams | Add-Member -MemberType NoteProperty -Name 'Title' -Value $ContactObject.Title } - if ($ContactObject.Company) { $SetContactParams | Add-Member -MemberType NoteProperty -Name 'Company' -Value $ContactObject.Company } - if ($ContactObject.StreetAddress) { $SetContactParams | Add-Member -MemberType NoteProperty -Name 'StreetAddress' -Value $ContactObject.StreetAddress } - if ($ContactObject.City) { $SetContactParams | Add-Member -MemberType NoteProperty -Name 'City' -Value $ContactObject.City } - if ($ContactObject.State) { $SetContactParams | Add-Member -MemberType NoteProperty -Name 'StateOrProvince' -Value $ContactObject.State } - if ($ContactObject.PostalCode) { $SetContactParams | Add-Member -MemberType NoteProperty -Name 'PostalCode' -Value $ContactObject.PostalCode } - if ($ContactObject.CountryOrRegion) { $SetContactParams | Add-Member -MemberType NoteProperty -Name 'CountryOrRegion' -Value $ContactObject.CountryOrRegion } - if ($ContactObject.phone) { $SetContactParams | Add-Member -MemberType NoteProperty -Name 'Phone' -Value $ContactObject.phone } - if ($ContactObject.mobilePhone) { $SetContactParams | Add-Member -MemberType NoteProperty -Name 'MobilePhone' -Value $ContactObject.mobilePhone } - if ($ContactObject.website) { $SetContactParams | Add-Member -MemberType NoteProperty -Name 'WebPage' -Value $ContactObject.website } + # Helper to add non-empty values + $PropertyMap = @{ + 'Title' = $ContactObject.Title + 'Company' = $ContactObject.Company + 'StreetAddress' = $ContactObject.StreetAddress + 'City' = $ContactObject.City + 'StateOrProvince' = $ContactObject.State + 'PostalCode' = $ContactObject.PostalCode + 'CountryOrRegion' = $ContactObject.CountryOrRegion + 'Phone' = $ContactObject.phone + 'MobilePhone' = $ContactObject.mobilePhone + 'WebPage' = $ContactObject.website + } + + # Add only non-null/non-empty properties + foreach ($Property in $PropertyMap.GetEnumerator()) { + if (![string]::IsNullOrWhiteSpace($Property.Value)) { + $SetContactParams[$Property.Key] = $Property.Value + } + } - # Update the contact with additional details - $null = New-ExoRequest -tenantid $TenantId -cmdlet 'Set-Contact' -cmdParams $SetContactParams -UseSystemMailbox $true + # Update the contact with additional details only if we have properties to set + if ($SetContactParams.Count -gt 1) { + $null = New-ExoRequest -tenantid $TenantId -cmdlet 'Set-Contact' -cmdParams $SetContactParams -UseSystemMailbox $true + } - # Set mail contact specific properties + # Build MailContact parameters efficiently $MailContactParams = @{ Identity = $NewContact.id - HiddenFromAddressListsEnabled = [boolean]$ContactObject.hidefromGAL + HiddenFromAddressListsEnabled = [bool]$ContactObject.hidefromGAL } # Add MailTip if provided - if ($ContactObject.mailTip) { + if (![string]::IsNullOrWhiteSpace($ContactObject.mailTip)) { $MailContactParams.MailTip = $ContactObject.mailTip } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-EditContact.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-EditContact.ps1 index 3446b866b074..3b3d458bcba2 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-EditContact.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-EditContact.ps1 @@ -20,9 +20,13 @@ Function Invoke-EditContact { # Extract contact information from the request body $contactInfo = $Request.Body - # Prepare the body for the Set-Contact cmdlet - $bodyForSetContact = [pscustomobject] @{ - 'Identity' = $contactInfo.ContactID + # Build contact parameters with only provided values + $bodyForSetContact = @{ + Identity = $contactInfo.ContactID + } + + # Map of properties to check and add + $ContactPropertyMap = @{ 'DisplayName' = $contactInfo.displayName 'WindowsEmailAddress' = $contactInfo.email 'FirstName' = $contactInfo.firstName @@ -39,24 +43,39 @@ Function Invoke-EditContact { 'WebPage' = $contactInfo.website } - # Call the Set-Contact cmdlet to update the contact - $null = New-ExoRequest -tenantid $TenantID -cmdlet 'Set-Contact' -cmdParams $bodyForSetContact -UseSystemMailbox $true + # Add only non-null/non-empty properties + foreach ($Property in $ContactPropertyMap.GetEnumerator()) { + if (![string]::IsNullOrWhiteSpace($Property.Value)) { + $bodyForSetContact[$Property.Key] = $Property.Value + } + } + + # Update contact only if we have properties to set beyond Identity + if ($bodyForSetContact.Count -gt 1) { + $null = New-ExoRequest -tenantid $TenantID -cmdlet 'Set-Contact' -cmdParams $bodyForSetContact -UseSystemMailbox $true + } # Prepare mail contact specific parameters $MailContactParams = @{ Identity = $contactInfo.ContactID - HiddenFromAddressListsEnabled = [System.Convert]::ToBoolean($contactInfo.hidefromGAL) + } + + # Handle boolean conversion safely + if ($null -ne $contactInfo.hidefromGAL) { + $MailContactParams.HiddenFromAddressListsEnabled = [bool]$contactInfo.hidefromGAL } # Add MailTip if provided - if ($contactInfo.mailTip) { + if (![string]::IsNullOrWhiteSpace($contactInfo.mailTip)) { $MailContactParams.MailTip = $contactInfo.mailTip } - # Update mail contact properties - $null = New-ExoRequest -tenantid $TenantID -cmdlet 'Set-MailContact' -cmdParams $MailContactParams -UseSystemMailbox $true + # Update mail contact only if we have properties to set beyond Identity + if ($MailContactParams.Count -gt 1) { + $null = New-ExoRequest -tenantid $TenantID -cmdlet 'Set-MailContact' -cmdParams $MailContactParams -UseSystemMailbox $true + } - $Results = "Successfully edited contact $($contactInfo.DisplayName)" + $Results = "Successfully edited contact $($contactInfo.displayName)" Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantID -message $Results -Sev Info $StatusCode = [HttpStatusCode]::OK } From d6c1d578a710f98656777b985d63621cbb5a0bb9 Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Sat, 31 May 2025 14:37:26 +0800 Subject: [PATCH 011/160] dirty retry but it rarely needed anyway --- .../Administration/Invoke-AddContact.ps1 | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-AddContact.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-AddContact.ps1 index 7cec494d892c..a957f07fea8f 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-AddContact.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-AddContact.ps1 @@ -58,7 +58,15 @@ Function Invoke-AddContact { # Update the contact with additional details only if we have properties to set if ($SetContactParams.Count -gt 1) { - $null = New-ExoRequest -tenantid $TenantId -cmdlet 'Set-Contact' -cmdParams $SetContactParams -UseSystemMailbox $true + for ($i = 1; $i -le 3; $i++) { + try { + $null = New-ExoRequest -tenantid $TenantId -cmdlet 'Set-Contact' -cmdParams $SetContactParams -UseSystemMailbox $true + break + } + catch { + if ($i -eq 3) { throw } + } + } } # Build MailContact parameters efficiently @@ -72,7 +80,15 @@ Function Invoke-AddContact { $MailContactParams.MailTip = $ContactObject.mailTip } - $null = New-ExoRequest -tenantid $TenantId -cmdlet 'Set-MailContact' -cmdParams $MailContactParams -UseSystemMailbox $true + for ($i = 1; $i -le 3; $i++) { + try { + $null = New-ExoRequest -tenantid $TenantId -cmdlet 'Set-MailContact' -cmdParams $MailContactParams -UseSystemMailbox $true + break + } + catch { + if ($i -eq 3) { throw } + } + } # Log the result $Result = "Successfully created contact $($ContactObject.displayName) with email address $($ContactObject.email)" From 1c8fd60e14182834b330a9c25e52abad6b870e1b Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Sat, 31 May 2025 15:36:39 +0800 Subject: [PATCH 012/160] sleep is better in this situation but alos check you actually need to hide from GAL as not hidden is default and most popular option --- .../Administration/Invoke-AddContact.ps1 | 34 ++++++++----------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-AddContact.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-AddContact.ps1 index a957f07fea8f..2568ccebe9e5 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-AddContact.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-AddContact.ps1 @@ -58,36 +58,32 @@ Function Invoke-AddContact { # Update the contact with additional details only if we have properties to set if ($SetContactParams.Count -gt 1) { - for ($i = 1; $i -le 3; $i++) { - try { - $null = New-ExoRequest -tenantid $TenantId -cmdlet 'Set-Contact' -cmdParams $SetContactParams -UseSystemMailbox $true - break - } - catch { - if ($i -eq 3) { throw } - } - } + Start-Sleep -Milliseconds 500 # Ensure the contact is created before updating + $null = New-ExoRequest -tenantid $TenantId -cmdlet 'Set-Contact' -cmdParams $SetContactParams -UseSystemMailbox $true } - # Build MailContact parameters efficiently + # Check if we need to update MailContact properties + $needsMailContactUpdate = $false $MailContactParams = @{ Identity = $NewContact.id - HiddenFromAddressListsEnabled = [bool]$ContactObject.hidefromGAL + } + + # Only add HiddenFromAddressListsEnabled if we're actually hiding from GAL + if ([bool]$ContactObject.hidefromGAL) { + $MailContactParams.HiddenFromAddressListsEnabled = $true + $needsMailContactUpdate = $true } # Add MailTip if provided if (![string]::IsNullOrWhiteSpace($ContactObject.mailTip)) { $MailContactParams.MailTip = $ContactObject.mailTip + $needsMailContactUpdate = $true } - for ($i = 1; $i -le 3; $i++) { - try { - $null = New-ExoRequest -tenantid $TenantId -cmdlet 'Set-MailContact' -cmdParams $MailContactParams -UseSystemMailbox $true - break - } - catch { - if ($i -eq 3) { throw } - } + # Only call Set-MailContact if we have changes to make + if ($needsMailContactUpdate) { + Start-Sleep -Milliseconds 500 # Ensure the contact is created before updating + $null = New-ExoRequest -tenantid $TenantId -cmdlet 'Set-MailContact' -cmdParams $MailContactParams -UseSystemMailbox $true } # Log the result From 1dd90336e2895389c0c290cf744f60a95d0db4ae Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Sat, 31 May 2025 19:42:32 +0800 Subject: [PATCH 013/160] Start of works on contact templates + new standard --- .../{ => Contacts}/Invoke-AddContact.ps1 | 0 .../Contacts/Invoke-AddContactTemplates.ps1 | 68 +++++ .../Invoke-DeployContactTemplates.ps1 | 0 .../{ => Contacts}/Invoke-EditContact.ps1 | 0 .../Contacts/Invoke-EditContactTemplates.ps1 | 0 .../Contacts/Invoke-ListContactTemplates.ps1 | 39 +++ .../{ => Contacts}/Invoke-ListContacts.ps1 | 0 .../{ => Contacts}/Invoke-RemoveContact.ps1 | 0 .../Invoke-RemoveContactTemplates.ps1 | 34 +++ .../Invoke-CIPPStandardDeployMailContact.ps1 | 2 +- ...voke-CIPPStandardDeployMailContactTemplate | 262 ++++++++++++++++++ ...oke-CIPPStandardMailboxRecipientLimits.ps1 | 6 +- 12 files changed, 409 insertions(+), 2 deletions(-) rename Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/{ => Contacts}/Invoke-AddContact.ps1 (100%) create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-AddContactTemplates.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-DeployContactTemplates.ps1 rename Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/{ => Contacts}/Invoke-EditContact.ps1 (100%) create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-EditContactTemplates.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-ListContactTemplates.ps1 rename Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/{ => Contacts}/Invoke-ListContacts.ps1 (100%) rename Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/{ => Contacts}/Invoke-RemoveContact.ps1 (100%) create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-RemoveContactTemplates.ps1 create mode 100644 Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContactTemplate diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-AddContact.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-AddContact.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-AddContact.ps1 rename to Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-AddContact.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-AddContactTemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-AddContactTemplates.ps1 new file mode 100644 index 000000000000..c90b320f8a22 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-AddContactTemplates.ps1 @@ -0,0 +1,68 @@ +using namespace System.Net + +Function Invoke-AddContactTemplate { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + Exchange.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -Headers $Headers -API $APINAME -message 'Accessed this API' -Sev Debug + Write-Host ($request | ConvertTo-Json -Depth 10 -Compress) + + try { + $GUID = (New-Guid).GUID + + # Create a new ordered hashtable to store selected properties + $contactObject = [ordered]@{} + + # Set name and comments first + $contactObject["name"] = $Request.body.Name ?? $Request.body.PolicyName ?? "Contact Template $(Get-Date -Format 'yyyy-MM-dd-HH-mm-ss')" + $contactObject["comments"] = $Request.body.Description ?? $Request.body.AdminDisplayName ?? "Contact template created $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" + + # Copy specific properties we want to keep + $propertiesToKeep = @( + "displayName", "firstName", "lastName", "email", "hidefromGAL", "streetAddress", "postalCode", + "city", "state", "country", "companyName", "mobilePhone", "businessPhone", "jobTitle", "website", "mailTip" + ) + + # Copy each property if it exists + foreach ($prop in $propertiesToKeep) { + if ($null -ne $Request.body.$prop) { + $contactObject[$prop] = $Request.body.$prop + } + } + + # Convert to JSON + $JSON = $contactObject | ConvertTo-Json -Depth 10 + + # Save the template to Azure Table Storage + $Table = Get-CippTable -tablename 'templates' + $Table.Force = $true + Add-CIPPAzDataTableEntity @Table -Entity @{ + JSON = "$JSON" + RowKey = "$GUID" + PartitionKey = 'ContactTemplate' + } + + Write-LogMessage -Headers $Headers -API $APINAME -message "Created Contact Template $($contactObject.name) with GUID $GUID" -Sev Info + $body = [pscustomobject]@{'Results' = "Created Contact Template $($contactObject.name) with GUID $GUID" } + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -Headers $Headers -API $APINAME -message "Failed to create Contact template: $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage + $body = [pscustomobject]@{'Results' = "Failed to create Contact template: $($ErrorMessage.NormalizedError)" } + $StatusCode = [HttpStatusCode]::Forbidden + } + + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = $body + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-DeployContactTemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-DeployContactTemplates.ps1 new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-EditContact.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-EditContact.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-EditContact.ps1 rename to Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-EditContact.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-EditContactTemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-EditContactTemplates.ps1 new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-ListContactTemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-ListContactTemplates.ps1 new file mode 100644 index 000000000000..f45acbae5786 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-ListContactTemplates.ps1 @@ -0,0 +1,39 @@ +using namespace System.Net +Function Invoke-ListContactTemplates { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + Exchange.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + $Table = Get-CippTable -tablename 'templates' + $Templates = Get-ChildItem 'Config\*.ContactTemplate.json' | ForEach-Object { + $Entity = @{ + JSON = "$(Get-Content $_)" + RowKey = "$($_.name)" + PartitionKey = 'ContactTemplate' + GUID = "$($_.name)" + } + Add-CIPPAzDataTableEntity @Table -Entity $Entity -Force + } + #List policies + $Table = Get-CippTable -tablename 'templates' + $Filter = "PartitionKey eq 'ContactTemplate'" + $Templates = (Get-CIPPAzDataTableEntity @Table -Filter $Filter) | ForEach-Object { + $GUID = $_.RowKey + $data = $_.JSON | ConvertFrom-Json + $data | Add-Member -NotePropertyName 'GUID' -NotePropertyValue $GUID + $data + } + if ($Request.query.ID) { $Templates = $Templates | Where-Object -Property RowKey -EQ $Request.query.id } + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = @($Templates) + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListContacts.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-ListContacts.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListContacts.ps1 rename to Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-ListContacts.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-RemoveContact.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-RemoveContact.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-RemoveContact.ps1 rename to Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-RemoveContact.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-RemoveContactTemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-RemoveContactTemplates.ps1 new file mode 100644 index 000000000000..a91155170324 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-RemoveContactTemplates.ps1 @@ -0,0 +1,34 @@ +using namespace System.Net +Function Invoke-RemoveContactTemplate { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + Exchange.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + $APIName = $Request.Params.CIPPEndpoint + $User = $Request.Headers + Write-LogMessage -Headers $User -API $APINAME -message 'Accessed this API' -Sev 'Debug' + $ID = $request.query.ID ?? $request.body.ID + try { + $Table = Get-CippTable -tablename 'templates' + $Filter = "PartitionKey eq 'ContactTemplate' and RowKey eq '$id'" + $ClearRow = Get-CIPPAzDataTableEntity @Table -Filter $Filter -Property PartitionKey, RowKey + Remove-AzDataTableEntity -Force @Table -Entity $ClearRow + $Result = "Removed Contact Template with ID $ID." + Write-LogMessage -Headers $User -API $APINAME -message $Result -Sev 'Info' + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to remove Contact template with ID $ID. Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -Headers $User -API $APINAME -message $Result -Sev 'Error' -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::Forbidden + } + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{ Results = $Result } + }) +} diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1 index 1802ac42847a..9a3b212639c9 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1 @@ -100,4 +100,4 @@ function Invoke-CIPPStandardDeployMailContact { Add-CIPPBPAField -FieldName 'DeployMailContact' -FieldValue $ReportData -StoreAs json -Tenant $Tenant Set-CIPPStandardsCompareField -FieldName 'standards.DeployMailContact' -FieldValue $($ExistingContact ? $true : $ReportData) -Tenant $Tenant } -} \ No newline at end of file +} diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContactTemplate b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContactTemplate new file mode 100644 index 000000000000..b1f3552b5d36 --- /dev/null +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContactTemplate @@ -0,0 +1,262 @@ +function Invoke-CIPPStandardDeployMailContactTemplate { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) DeployMailContactTemplate + .SYNOPSIS + (Label) Deploy Mail Contact Template + .DESCRIPTION + (Helptext) Creates a new mail contact in Exchange Online across all selected tenants. The contact will be visible in the Global Address List unless hidden. + (DocsDescription) This standard creates a new mail contact in Exchange Online. Mail contacts are useful for adding external email addresses to your organization's address book. They can be used for distribution lists, shared mailboxes, and other collaboration scenarios. + .NOTES + CAT + Exchange Standards + TAG + ADDEDCOMPONENT + {"type":"textField","name":"ExternalEmailAddress","label":"External Email Address","required":true} + {"type":"textField","name":"DisplayName","label":"Display Name","required":true} + {"type":"textField","name":"FirstName","label":"First Name","required":false} + {"type":"textField","name":"LastName","label":"Last Name","required":false} + {"type":"textField","name":"Company","label":"Company","required":false} + {"type":"textField","name":"Office","label":"Office","required":false} + {"type":"textField","name":"State","label":"State","required":false} + {"type":"textField","name":"Phone","label":"Phone Number","required":false} + {"type":"textField","name":"Website","label":"Website","required":false} + {"type":"textField","name":"MailTip","label":"Mail Tip","required":false} + {"type":"switch","name":"HideFromGAL","label":"Hide from Global Address List"} + MULTIPLE + True + IMPACT + Low Impact + ADDEDDATE + 2024-03-19 + POWERSHELLEQUIVALENT + New-MailContact + RECOMMENDEDBY + "CIPP" + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + .LINK + https://docs.cipp.app/user-documentation/tenant/standards/list-standards + #> + + param($Tenant, $Settings) + + $APIName = 'Standards' + + # Helper function to get value from field (handles both string and {label,value} object) + function Get-FieldValue($field) { + if ($field -is [string]) { + return $field + } elseif ($field.value) { + return $field.value + } else { + return "" + } + } + + try { + Write-LogMessage -API $APIName -tenant $Tenant -message "DeployMailContactTemplate: Processing $($Settings.Count) contact(s)" -sev Info + + # Get the current contacts + $CurrentContacts = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-MailContact' -ErrorAction Stop + + # Compare the settings from standard with the current contacts + $CompareList = foreach ($Contact in $Settings) { + try { + # Extract values using helper function + $displayName = Get-FieldValue $Contact.DisplayName + $emailAddress = Get-FieldValue $Contact.ExternalEmailAddress + + # Input validation for required fields + if ([string]::IsNullOrWhiteSpace($displayName)) { + Write-LogMessage -API $APIName -tenant $Tenant -message "DeployMailContactTemplate: DisplayName cannot be empty for contact." -sev Error + continue + } + + if ([string]::IsNullOrWhiteSpace($emailAddress)) { + Write-LogMessage -API $APIName -tenant $Tenant -message "DeployMailContactTemplate: ExternalEmailAddress cannot be empty for contact $displayName." -sev Error + continue + } + + # Validate email address format + try { + $null = [System.Net.Mail.MailAddress]::new($emailAddress) + } + catch { + Write-LogMessage -API $APIName -tenant $Tenant -message "DeployMailContactTemplate: Invalid email address format: $emailAddress" -sev Error + continue + } + + # Check if the contact already exists + $ExistingContact = $CurrentContacts | Where-Object { $_.ExternalEmailAddress -eq $emailAddress } + + # Create hashtable with desired contact settings + $ContactData = @{ + DisplayName = $displayName + ExternalEmailAddress = $emailAddress + FirstName = Get-FieldValue $Contact.FirstName + LastName = Get-FieldValue $Contact.LastName + Company = Get-FieldValue $Contact.Company + Office = Get-FieldValue $Contact.Office + State = Get-FieldValue $Contact.State + Phone = Get-FieldValue $Contact.Phone + Website = Get-FieldValue $Contact.Website + MailTip = Get-FieldValue $Contact.MailTip + HideFromGAL = [bool]$Contact.HideFromGAL + } + + # If the contact already exists, check if it matches current settings + if ($ExistingContact) { + # For now, we'll consider existing contacts as "correct" - could add comparison logic here later + $StateIsCorrect = $true + $Action = "None" + $Missing = $false + } + else { + # Contact doesn't exist, needs to be created + $StateIsCorrect = $false + $Action = "Create" + $Missing = $true + } + + [PSCustomObject]@{ + missing = $Missing + StateIsCorrect = $StateIsCorrect + Action = $Action + ContactData = $ContactData + remediate = $Contact.remediate + alert = $Contact.alert + report = $Contact.report + } + } + catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + $Message = "Failed to compare contact $($Contact.DisplayName), Error: $ErrorMessage" + Write-LogMessage -API $APIName -tenant $tenant -message $Message -sev 'Error' + Return $Message + } + } + + # Remediate each contact which is incorrect or missing + If ($true -in $Settings.remediate) { + foreach ($Contact in $CompareList | Where-Object { $_.remediate -EQ $true -and $_.StateIsCorrect -eq $false }) { + try { + $ContactInfo = $Contact.ContactData + + # Parameters for splatting to create contact + $NewContactParams = @{ + displayName = $ContactInfo.DisplayName + name = $ContactInfo.DisplayName + ExternalEmailAddress = $ContactInfo.ExternalEmailAddress + } + + # Add optional name fields if provided + if (![string]::IsNullOrWhiteSpace($ContactInfo.FirstName)) { + $NewContactParams.FirstName = $ContactInfo.FirstName + } + if (![string]::IsNullOrWhiteSpace($ContactInfo.LastName)) { + $NewContactParams.LastName = $ContactInfo.LastName + } + + try { + # Create the mail contact first + $NewContact = New-ExoRequest -tenantid $Tenant -cmdlet 'New-MailContact' -cmdParams $NewContactParams -UseSystemMailbox $true + + # Build SetContactParams efficiently with only provided values + $SetContactParams = @{ + Identity = $NewContact.id + } + + # Helper to add non-empty values for Set-Contact + $PropertyMap = @{ + 'Company' = $ContactInfo.Company + 'StateOrProvince' = $ContactInfo.State + 'Office' = $ContactInfo.Office + 'Phone' = $ContactInfo.Phone + 'WebPage' = $ContactInfo.Website + } + + # Add only non-null/non-empty properties + foreach ($Property in $PropertyMap.GetEnumerator()) { + if (![string]::IsNullOrWhiteSpace($Property.Value)) { + $SetContactParams[$Property.Key] = $Property.Value + } + } + + # Update the contact with additional details only if we have properties to set + if ($SetContactParams.Count -gt 1) { + Start-Sleep -Milliseconds 500 # Ensure the contact is created before updating + $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-Contact' -cmdParams $SetContactParams -UseSystemMailbox $true + } + + # Check if we need to update MailContact properties + $needsMailContactUpdate = $false + $MailContactParams = @{ + Identity = $NewContact.id + } + + # Hide from GAL if requested + if ([bool]$ContactInfo.HideFromGAL) { + $MailContactParams.HiddenFromAddressListsEnabled = $true + $needsMailContactUpdate = $true + } + + # Add MailTip if provided + if (![string]::IsNullOrWhiteSpace($ContactInfo.MailTip)) { + $MailContactParams.MailTip = $ContactInfo.MailTip + $needsMailContactUpdate = $true + } + + # Only call Set-MailContact if we have changes to make + if ($needsMailContactUpdate) { + Start-Sleep -Milliseconds 500 # Ensure the contact is created before updating + $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-MailContact' -cmdParams $MailContactParams -UseSystemMailbox $true + } + + Write-LogMessage -API $APIName -tenant $Tenant -message "$($Contact.Action)d mail contact '$($ContactInfo.DisplayName)' with email '$($ContactInfo.ExternalEmailAddress)'" -sev Info + } + catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API $APIName -tenant $tenant -message "Failed to $($Contact.Action) contact $($ContactInfo.DisplayName), Error: $ErrorMessage" -sev 'Error' + } + } + catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API $APIName -tenant $tenant -message "Failed to create or update contact $($Contact.ContactData.DisplayName), Error: $ErrorMessage" -sev 'Error' + } + } + } + + if ($true -in $Settings.alert) { + foreach ($Contact in $CompareList | Where-Object -Property alert -EQ $true) { + if ($Contact.StateIsCorrect) { + Write-LogMessage -API $APIName -tenant $Tenant -message "Mail contact $($Contact.ContactData.DisplayName) has the correct configuration." -sev Info + } + else { + if ($Contact.missing) { + $CurrentInfo = $Contact.ContactData | Select-Object -Property DisplayName, ExternalEmailAddress, missing + Write-StandardsAlert -message "Mail contact $($Contact.ContactData.DisplayName) is missing." -object $CurrentInfo -tenant $Tenant -standardName 'DeployMailContact' + Write-LogMessage -API $APIName -tenant $Tenant -message "Mail contact $($Contact.ContactData.DisplayName) is missing." -sev info + } + else { + $CurrentInfo = $CurrentContacts | Where-Object -Property ExternalEmailAddress -eq $Contact.ContactData.ExternalEmailAddress | Select-Object -Property DisplayName, ExternalEmailAddress, FirstName, LastName + Write-StandardsAlert -message "Mail contact $($Contact.ContactData.DisplayName) does not match the expected configuration." -object $CurrentInfo -tenant $Tenant -standardName 'DeployMailContact' + Write-LogMessage -API $APIName -tenant $Tenant -message "Mail contact $($Contact.ContactData.DisplayName) does not match the expected configuration. We've generated an alert" -sev info + } + } + } + } + + if ($true -in $Settings.report) { + foreach ($Contact in $CompareList | Where-Object -Property report -EQ $true) { + Set-CIPPStandardsCompareField -FieldName "standards.DeployMailContact" -FieldValue $Contact.StateIsCorrect -TenantFilter $Tenant + } + } + } + catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API $APIName -tenant $tenant -message "Failed to create or update mail contact(s), Error: $ErrorMessage" -sev 'Error' + } +} diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1 index fca1c64d74ed..808774259f0f 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1 @@ -23,6 +23,10 @@ function Invoke-CIPPStandardMailboxRecipientLimits { Set-Mailbox -RecipientLimits RECOMMENDEDBY "CIPP" + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + .LINK + https://docs.cipp.app/user-documentation/tenant/standards/list-standards #> param($Tenant, $Settings) @@ -177,4 +181,4 @@ function Invoke-CIPPStandardMailboxRecipientLimits { } Set-CIPPStandardsCompareField -FieldName 'standards.MailboxRecipientLimits' -FieldValue $FieldValue -Tenant $Tenant } -} \ No newline at end of file +} From c7ec71ba40eb800aaa55379d2157d78c96447f77 Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Sat, 31 May 2025 20:49:13 +0800 Subject: [PATCH 014/160] Working contact template setup --- .../Contacts/Invoke-AddContactTemplates.ps1 | 6 +- .../Invoke-DeployContactTemplates.ps1 | 184 ++++++++++++++++++ .../Contacts/Invoke-EditContactTemplates.ps1 | 84 ++++++++ .../Contacts/Invoke-ListContactTemplates.ps1 | 45 ++++- .../Invoke-RemoveContactTemplates.ps1 | 5 +- openapi.json | 83 ++++++++ 6 files changed, 394 insertions(+), 13 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-AddContactTemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-AddContactTemplates.ps1 index c90b320f8a22..0537e503e980 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-AddContactTemplates.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-AddContactTemplates.ps1 @@ -1,6 +1,6 @@ using namespace System.Net -Function Invoke-AddContactTemplate { +Function Invoke-AddContactTemplates { <# .FUNCTIONALITY Entrypoint,AnyTenant @@ -22,8 +22,8 @@ Function Invoke-AddContactTemplate { $contactObject = [ordered]@{} # Set name and comments first - $contactObject["name"] = $Request.body.Name ?? $Request.body.PolicyName ?? "Contact Template $(Get-Date -Format 'yyyy-MM-dd-HH-mm-ss')" - $contactObject["comments"] = $Request.body.Description ?? $Request.body.AdminDisplayName ?? "Contact template created $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" + $contactObject["name"] = $Request.body.displayName + $contactObject["comments"] = "Contact template created $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" # Copy specific properties we want to keep $propertiesToKeep = @( diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-DeployContactTemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-DeployContactTemplates.ps1 index e69de29bb2d1..14fa69806c3f 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-DeployContactTemplates.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-DeployContactTemplates.ps1 @@ -0,0 +1,184 @@ +using namespace System.Net + +Function Invoke-DeployContactTemplates { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Exchange.Contact.ReadWrite + .DESCRIPTION + This function deploys contact(s) from template(s) to selected tenants. + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + + try { + $RequestBody = $Request.Body + + # Extract tenant IDs from the selectedTenants objects - get the value property + $SelectedTenants = [System.Collections.Generic.List[string]]::new() + + foreach ($TenantItem in $RequestBody.selectedTenants) { + if ($TenantItem.value) { + $SelectedTenants.Add($TenantItem.value) + } else { + Write-LogMessage -headers $Headers -API $APIName -message "Tenant item missing value property: $($TenantItem | ConvertTo-Json -Compress)" -Sev 'Warning' + } + } + + # Handle AllTenants selection + if ('AllTenants' -in $SelectedTenants) { + $SelectedTenants = [System.Collections.Generic.List[string]]::new() + $AllTenantsList = (Get-Tenants).defaultDomainName + foreach ($Tenant in $AllTenantsList) { + $SelectedTenants.Add($Tenant) + } + } + + # Get the contact templates from TemplateList + $ContactTemplates = [System.Collections.Generic.List[object]]::new() + + if ($RequestBody.TemplateList -and $RequestBody.TemplateList.Count -gt 0) { + # Templates are provided in TemplateList format + foreach ($TemplateItem in $RequestBody.TemplateList) { + if ($TemplateItem.value) { + $ContactTemplates.Add($TemplateItem.value) + } else { + Write-LogMessage -headers $Headers -API $APIName -message "Template item missing value property: $($TemplateItem | ConvertTo-Json -Compress)" -Sev 'Warning' + } + } + } else { + throw "TemplateList is required and must contain at least one template" + } + + if ($ContactTemplates.Count -eq 0) { + throw "No valid contact templates found to deploy" + } + + $Results = foreach ($TenantFilter in $SelectedTenants) { + foreach ($ContactTemplate in $ContactTemplates) { + try { + # Check if contact with this email already exists + $ExistingContactsParam = @{ + tenantid = $TenantFilter + cmdlet = 'Get-MailContact' + cmdParams = @{ + Filter = "ExternalEmailAddress -eq '$($ContactTemplate.email)'" + } + useSystemMailbox = $true + } + + $ExistingContacts = New-ExoRequest @ExistingContactsParam + $ContactExists = $ExistingContacts | Where-Object { $_.ExternalEmailAddress -eq $ContactTemplate.email } + + if ($ContactExists) { + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Contact with email '$($ContactTemplate.email)' already exists in tenant $TenantFilter" -Sev 'Warning' + "Contact '$($ContactTemplate.displayName)' with email '$($ContactTemplate.email)' already exists in tenant $TenantFilter" + continue + } + + # Prepare the body for New-MailContact cmdlet + $BodyToship = @{ + displayName = $ContactTemplate.displayName + name = $ContactTemplate.displayName + ExternalEmailAddress = $ContactTemplate.email + FirstName = $ContactTemplate.firstName + LastName = $ContactTemplate.lastName + } + + # Create the mail contact first + $NewContact = New-ExoRequest -tenantid $TenantFilter -cmdlet 'New-MailContact' -cmdParams $BodyToship -UseSystemMailbox $true + + # Build SetContactParams efficiently with only provided values + $SetContactParams = @{ + Identity = $NewContact.id + } + + # Helper to add non-empty values + $PropertyMap = @{ + 'Title' = $ContactTemplate.jobTitle + 'Company' = $ContactTemplate.companyName + 'StreetAddress' = $ContactTemplate.streetAddress + 'City' = $ContactTemplate.city + 'StateOrProvince' = $ContactTemplate.state + 'PostalCode' = $ContactTemplate.postalCode + 'CountryOrRegion' = $ContactTemplate.country + 'Phone' = $ContactTemplate.businessPhone + 'MobilePhone' = $ContactTemplate.mobilePhone + 'WebPage' = $ContactTemplate.website + } + + # Add only non-null/non-empty properties + foreach ($Property in $PropertyMap.GetEnumerator()) { + if (![string]::IsNullOrWhiteSpace($Property.Value)) { + $SetContactParams[$Property.Key] = $Property.Value + } + } + + # Update the contact with additional details only if we have properties to set + if ($SetContactParams.Count -gt 1) { + Start-Sleep -Milliseconds 500 # Ensure the contact is created before updating + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Set-Contact' -cmdParams $SetContactParams -UseSystemMailbox $true + } + + # Check if we need to update MailContact properties + $needsMailContactUpdate = $false + $MailContactParams = @{ + Identity = $NewContact.id + } + + # Only add HiddenFromAddressListsEnabled if we're actually hiding from GAL + if ([bool]$ContactTemplate.hidefromGAL) { + $MailContactParams.HiddenFromAddressListsEnabled = $true + $needsMailContactUpdate = $true + } + + # Add MailTip if provided + if (![string]::IsNullOrWhiteSpace($ContactTemplate.mailTip)) { + $MailContactParams.MailTip = $ContactTemplate.mailTip + $needsMailContactUpdate = $true + } + + # Only call Set-MailContact if we have changes to make + if ($needsMailContactUpdate) { + Start-Sleep -Milliseconds 500 # Ensure the contact is created before updating + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Set-MailContact' -cmdParams $MailContactParams -UseSystemMailbox $true + } + + # Log the result + $ContactResult = "Successfully created contact '$($ContactTemplate.displayName)' with email '$($ContactTemplate.email)'" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $ContactResult -Sev 'Info' + + # Return success message as a simple string + "Successfully deployed contact '$($ContactTemplate.displayName)' to tenant $TenantFilter" + } + catch { + $ErrorMessage = Get-CippException -Exception $_ + $ErrorDetail = "Failed to deploy contact '$($ContactTemplate.displayName)' to tenant $TenantFilter. Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $ErrorDetail -Sev 'Error' + + # Return error message as a simple string + "Failed to deploy contact '$($ContactTemplate.displayName)' to tenant $TenantFilter. Error: $($ErrorMessage.NormalizedError)" + } + } + } + + $StatusCode = [HttpStatusCode]::OK + } + catch { + $ErrorMessage = Get-CippException -Exception $_ + $Results = "Failed to process contact template deployment request. Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -message $Results -Sev 'Error' + $StatusCode = [HttpStatusCode]::InternalServerError + } + + # 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/Email-Exchange/Administration/Contacts/Invoke-EditContactTemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-EditContactTemplates.ps1 index e69de29bb2d1..de2770151466 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-EditContactTemplates.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-EditContactTemplates.ps1 @@ -0,0 +1,84 @@ +using namespace System.Net + +Function Invoke-EditContactTemplates { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + Exchange.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -Headers $Headers -API $APINAME -message 'Accessed this API' -Sev Debug + Write-Host ($request | ConvertTo-Json -Depth 10 -Compress) + + try { + # Get the ContactTemplateID from the request body + $ContactTemplateID = $Request.body.ContactTemplateID + + if (-not $ContactTemplateID) { + throw "ContactTemplateID is required for editing a template" + } + + # Check if the template exists + $Table = Get-CippTable -tablename 'templates' + $Filter = "PartitionKey eq 'ContactTemplate' and RowKey eq '$ContactTemplateID'" + $ExistingTemplate = Get-CIPPAzDataTableEntity @Table -Filter $Filter + + if (-not $ExistingTemplate) { + throw "Contact template with ID $ContactTemplateID not found" + } + + Write-LogMessage -Headers $Headers -API $APINAME -message "Updating Contact Template with ID: $ContactTemplateID" -Sev Info + + # Create a new ordered hashtable to store selected properties + $contactObject = [ordered]@{} + + # Set name and comments + $contactObject["name"] = $Request.body.displayName + $contactObject["comments"] = "Contact template updated $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" + + # Copy specific properties we want to keep + $propertiesToKeep = @( + "displayName", "firstName", "lastName", "email", "hidefromGAL", "streetAddress", "postalCode", + "city", "state", "country", "companyName", "mobilePhone", "businessPhone", "jobTitle", "website", "mailTip" + ) + + # Copy each property from the request + foreach ($prop in $propertiesToKeep) { + if ($null -ne $Request.body.$prop) { + $contactObject[$prop] = $Request.body.$prop + } + } + + # Convert to JSON + $JSON = $contactObject | ConvertTo-Json -Depth 10 + + # Overwrite the template in Azure Table Storage + $Table = Get-CippTable -tablename 'templates' + $Table.Force = $true + Add-CIPPAzDataTableEntity @Table -Entity @{ + JSON = "$JSON" + RowKey = "$ContactTemplateID" + PartitionKey = 'ContactTemplate' + } + + Write-LogMessage -Headers $Headers -API $APINAME -message "Updated Contact Template $($contactObject.name) with GUID $ContactTemplateID" -Sev Info + $body = [pscustomobject]@{'Results' = "Updated Contact Template $($contactObject.name) with GUID $ContactTemplateID" } + $StatusCode = [HttpStatusCode]::OK + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -Headers $Headers -API $APINAME -message "Failed to update Contact template: $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage + $body = [pscustomobject]@{'Results' = "Failed to update Contact template: $($ErrorMessage.NormalizedError)" } + $StatusCode = [HttpStatusCode]::Forbidden + } + + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = $body + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-ListContactTemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-ListContactTemplates.ps1 index f45acbae5786..7bd3bb63ef74 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-ListContactTemplates.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-ListContactTemplates.ps1 @@ -11,6 +11,7 @@ Function Invoke-ListContactTemplates { $APIName = $Request.Params.CIPPEndpoint $Headers = $Request.Headers Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + $Table = Get-CippTable -tablename 'templates' $Templates = Get-ChildItem 'Config\*.ContactTemplate.json' | ForEach-Object { $Entity = @{ @@ -21,16 +22,42 @@ Function Invoke-ListContactTemplates { } Add-CIPPAzDataTableEntity @Table -Entity $Entity -Force } - #List policies - $Table = Get-CippTable -tablename 'templates' - $Filter = "PartitionKey eq 'ContactTemplate'" - $Templates = (Get-CIPPAzDataTableEntity @Table -Filter $Filter) | ForEach-Object { - $GUID = $_.RowKey - $data = $_.JSON | ConvertFrom-Json - $data | Add-Member -NotePropertyName 'GUID' -NotePropertyValue $GUID - $data + + # Check if a specific template ID is requested + if ($Request.query.ID -or $Request.query.id) { + $RequestedID = $Request.query.ID ?? $Request.query.id + Write-LogMessage -headers $Headers -API $APIName -message "Retrieving specific template with ID: $RequestedID" -Sev 'Debug' + + # Query directly for the specific template by RowKey for efficiency + $Filter = "PartitionKey eq 'ContactTemplate' and RowKey eq '$RequestedID'" + $Templates = (Get-CIPPAzDataTableEntity @Table -Filter $Filter) | ForEach-Object { + $GUID = $_.RowKey + $data = $_.JSON | ConvertFrom-Json + $data | Add-Member -NotePropertyName 'GUID' -NotePropertyValue $GUID + $data + } + + if (-not $Templates) { + Write-LogMessage -headers $Headers -API $APIName -message "Template with ID $RequestedID not found" -Sev 'Warning' + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::NotFound + Body = @{ Error = "Template with ID $RequestedID not found" } + }) + return + } + } else { + # List all policies if no specific ID requested + Write-LogMessage -headers $Headers -API $APIName -message 'Retrieving all contact templates' -Sev 'Debug' + + $Filter = "PartitionKey eq 'ContactTemplate'" + $Templates = (Get-CIPPAzDataTableEntity @Table -Filter $Filter) | ForEach-Object { + $GUID = $_.RowKey + $data = $_.JSON | ConvertFrom-Json + $data | Add-Member -NotePropertyName 'GUID' -NotePropertyValue $GUID + $data + } } - if ($Request.query.ID) { $Templates = $Templates | Where-Object -Property RowKey -EQ $Request.query.id } + # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::OK diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-RemoveContactTemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-RemoveContactTemplates.ps1 index a91155170324..137b03e18a9b 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-RemoveContactTemplates.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-RemoveContactTemplates.ps1 @@ -1,5 +1,6 @@ using namespace System.Net -Function Invoke-RemoveContactTemplate { + +Function Invoke-RemoveContactTemplates { <# .FUNCTIONALITY Entrypoint,AnyTenant @@ -10,8 +11,10 @@ Function Invoke-RemoveContactTemplate { param($Request, $TriggerMetadata) $APIName = $Request.Params.CIPPEndpoint $User = $Request.Headers + Write-LogMessage -Headers $User -API $APINAME -message 'Accessed this API' -Sev 'Debug' $ID = $request.query.ID ?? $request.body.ID + try { $Table = Get-CippTable -tablename 'templates' $Filter = "PartitionKey eq 'ContactTemplate' and RowKey eq '$id'" diff --git a/openapi.json b/openapi.json index 7fb2383fa0cc..261f5778fe1b 100644 --- a/openapi.json +++ b/openapi.json @@ -3557,6 +3557,89 @@ } } }, + "/ListContactTemplates": { + "get": { + "description": "List Contact Templates", + "summary": "List Contact Templates", + "tags": ["GET"], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": {} + } + } + } + }, + "description": "Successful operation" + } + } + } + }, + "/RemoveContactTemplates": { + "get": { + "description": "Remove Contact Template", + "summary": "Remove Contact Template", + "tags": ["GET"], + "parameters": [ + { + "required": true, + "schema": { + "type": "string" + }, + "name": "id", + "in": "query" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": {}, + "type": "object" + } + } + }, + "description": "Successful operation" + } + } + } + }, + "/AddContactTemplates": { + "post": { + "description": "Add Contact Template", + "summary": "Add Contact Template", + "tags": ["POST"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {} + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": {}, + "type": "object" + } + } + }, + "description": "Successful operation" + } + } + } + }, "/ListMailboxRules": { "get": { "description": "ListMailboxRules", From 226bdacc9050d8ee5eadec3a35fbfe623433ce66 Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Sat, 31 May 2025 23:40:46 +0800 Subject: [PATCH 015/160] Tidy up and naming --- ...oke-CIPPStandardDeployContactTemplates.ps1 | 362 ++++++++++++++++++ ...voke-CIPPStandardDeployMailContactTemplate | 262 ------------- 2 files changed, 362 insertions(+), 262 deletions(-) create mode 100644 Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 delete mode 100644 Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContactTemplate diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 new file mode 100644 index 000000000000..390ad4b64fe0 --- /dev/null +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 @@ -0,0 +1,362 @@ +function Invoke-CIPPStandardDeployContactTemplates { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) DeployContactTemplates + .SYNOPSIS + (Label) Deploy Contact Templates + .DESCRIPTION + (Helptext) Creates a new contacts in Exchange Online across all selected tenants from saved contact templates. The contact will be visible in the Global Address List unless hidden. + (DocsDescription) This standard creates new contacts in Exchange Online from saved contact templates. Mail contacts are useful for adding external email addresses to your organization's address book. They can be used for distribution lists, shared mailboxes, and other collaboration scenarios. + .NOTES + CAT + Exchange Standards + TAG + ADDEDCOMPONENT + {"type":"textField","name":"TemplateGUID","label":"Contact Template GUID","required":true} + MULTIPLE + True + IMPACT + Low Impact + ADDEDDATE + 2024-03-19 + POWERSHELLEQUIVALENT + New-MailContact + RECOMMENDEDBY + "CIPP" + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + .LINK + https://docs.cipp.app/user-documentation/tenant/standards/list-standards + #> + + param($Tenant, $Settings) + + $APIName = 'Standards' + + + + # Helper function to get template by GUID + function Get-ContactTemplate($TemplateGUID) { + try { + $Table = Get-CippTable -tablename 'templates' + $Filter = "PartitionKey eq 'ContactTemplate' and RowKey eq '$TemplateGUID'" + $StoredTemplate = Get-CIPPAzDataTableEntity @Table -Filter $Filter + + if (-not $StoredTemplate) { + Write-LogMessage -API $APIName -tenant $Tenant -message "Contact template with GUID $TemplateGUID not found" -sev Error + return $null + } + + return $StoredTemplate.JSON | ConvertFrom-Json + } + catch { + Write-LogMessage -API $APIName -tenant $Tenant -message "Failed to retrieve template $TemplateGUID. Error: $($_.Exception.Message)" -sev Error + return $null + } + } + + + + try { + # Extract control flags from Settings + $RemediateEnabled = [bool]$Settings.remediate + $AlertEnabled = [bool]$Settings.alert + $ReportEnabled = [bool]$Settings.report + + # Get templateIds array + if (-not $Settings.templateIds -or $Settings.templateIds.Count -eq 0) { + Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: No template IDs found in settings" -sev Error + return "No template IDs found in settings" + } + + Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: Processing $($Settings.templateIds.Count) template(s)" -sev Info + + # Get the current contacts + $CurrentContacts = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-MailContact' -ErrorAction Stop + + # Process each template in the templateIds array + $CompareList = foreach ($TemplateItem in $Settings.templateIds) { + try { + # Get the template GUID directly from the value property + $TemplateGUID = $TemplateItem.value + + if ([string]::IsNullOrWhiteSpace($TemplateGUID)) { + Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: TemplateGUID cannot be empty." -sev Error + continue + } + + # Fetch the template from storage + $Template = Get-ContactTemplate -TemplateGUID $TemplateGUID + if (-not $Template) { + continue + } + + # Input validation for required fields + if ([string]::IsNullOrWhiteSpace($Template.displayName)) { + Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: DisplayName cannot be empty for template $TemplateGUID." -sev Error + continue + } + + if ([string]::IsNullOrWhiteSpace($Template.email)) { + Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: ExternalEmailAddress cannot be empty for template $TemplateGUID." -sev Error + continue + } + + # Validate email address format + try { + $null = [System.Net.Mail.MailAddress]::new($Template.email) + } + catch { + Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: Invalid email address format: $($Template.email)" -sev Error + continue + } + + # Check if the contact already exists (using DisplayName as key) + $ExistingContact = $CurrentContacts | Where-Object { $_.DisplayName -eq $Template.displayName } + + # If the contact exists, we'll overwrite it; if not, we'll create it + if ($ExistingContact) { + $StateIsCorrect = $false # Always update existing contacts to match template + $Action = "Update" + $Missing = $false + } + else { + # Contact doesn't exist, needs to be created + $StateIsCorrect = $false + $Action = "Create" + $Missing = $true + } + + [PSCustomObject]@{ + missing = $Missing + StateIsCorrect = $StateIsCorrect + Action = $Action + Template = $Template + TemplateGUID = $TemplateGUID + } + } + catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + $Message = "Failed to process template $TemplateGUID, Error: $ErrorMessage" + Write-LogMessage -API $APIName -tenant $tenant -message $Message -sev 'Error' + Return $Message + } + } + + # Remediate each contact which needs to be created or updated + If ($RemediateEnabled) { + $ContactsToProcess = $CompareList | Where-Object { $_.StateIsCorrect -eq $false } + + if ($ContactsToProcess.Count -gt 0) { + $ContactsToCreate = $ContactsToProcess | Where-Object { $_.Action -eq "Create" } + $ContactsToUpdate = $ContactsToProcess | Where-Object { $_.Action -eq "Update" } + + Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: Processing $($ContactsToCreate.Count) new contacts, $($ContactsToUpdate.Count) existing contacts" -sev Info + + # First pass: Create new mail contacts and update existing ones + $ProcessedContacts = [System.Collections.Generic.List[PSCustomObject]]::new() + $ProcessingFailures = 0 + + # Handle new contacts + foreach ($Contact in $ContactsToCreate) { + try { + $Template = $Contact.Template + + # Parameters for creating new contact + $NewContactParams = @{ + displayName = $Template.displayName + name = $Template.displayName + ExternalEmailAddress = $Template.email + } + + # Add optional name fields if provided + if (![string]::IsNullOrWhiteSpace($Template.firstName)) { + $NewContactParams.FirstName = $Template.firstName + } + if (![string]::IsNullOrWhiteSpace($Template.lastName)) { + $NewContactParams.LastName = $Template.lastName + } + + # Create the mail contact + $NewContact = New-ExoRequest -tenantid $Tenant -cmdlet 'New-MailContact' -cmdParams $NewContactParams -UseSystemMailbox $true + + # Store contact info for second pass + $ProcessedContacts.Add([PSCustomObject]@{ + Contact = $Contact + ContactObject = $NewContact + Template = $Template + IsNew = $true + }) + } + catch { + $ProcessingFailures++ + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API $APIName -tenant $tenant -message "Failed to create contact $($Template.displayName): $ErrorMessage" -sev 'Error' + } + } + + # Handle existing contacts - update their basic properties + foreach ($Contact in $ContactsToUpdate) { + try { + $Template = $Contact.Template + $ExistingContact = $CurrentContacts | Where-Object { $_.DisplayName -eq $Template.displayName } + + # Update basic MailContact properties + $UpdateContactParams = @{ + Identity = $ExistingContact.Identity + ExternalEmailAddress = $Template.email + } + + # Add optional name fields if provided + if (![string]::IsNullOrWhiteSpace($Template.firstName)) { + $UpdateContactParams.FirstName = $Template.firstName + } + if (![string]::IsNullOrWhiteSpace($Template.lastName)) { + $UpdateContactParams.LastName = $Template.lastName + } + + # Update the existing mail contact + $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-MailContact' -cmdParams $UpdateContactParams -UseSystemMailbox $true + + # Store contact info for second pass + $ProcessedContacts.Add([PSCustomObject]@{ + Contact = $Contact + ContactObject = $ExistingContact + Template = $Template + IsNew = $false + }) + } + catch { + $ProcessingFailures++ + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API $APIName -tenant $tenant -message "Failed to update contact $($Template.displayName): $ErrorMessage" -sev 'Error' + } + } + + # Log processing summary + $ProcessedCount = $ProcessedContacts.Count + if ($ProcessedCount -gt 0) { + Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: Successfully processed $ProcessedCount contacts" -sev Info + + # Wait for contacts to propagate before updating additional fields + Start-Sleep -Seconds 1 + + # Second pass: Update contacts with additional fields (only if needed) + $UpdateFailures = 0 + $ContactsRequiringUpdates = 0 + + foreach ($ProcessedContactInfo in $ProcessedContacts) { + try { + $Template = $ProcessedContactInfo.Template + $ContactObject = $ProcessedContactInfo.ContactObject + $HasUpdates = $false + + # Check if Set-Contact is needed + $ContactIdentity = if ($ProcessedContactInfo.IsNew) { $ContactObject.id } else { $ContactObject.Identity } + $SetContactParams = @{ Identity = $ContactIdentity } + $PropertyMap = @{ + 'Company' = $Template.companyName + 'StateOrProvince' = $Template.state + 'Office' = $Template.streetAddress + 'Phone' = $Template.businessPhone + 'WebPage' = $Template.website + 'Title' = $Template.jobTitle + 'City' = $Template.city + 'PostalCode' = $Template.postalCode + 'CountryOrRegion' = $Template.country + 'MobilePhone' = $Template.mobilePhone + } + + foreach ($Property in $PropertyMap.GetEnumerator()) { + if (![string]::IsNullOrWhiteSpace($Property.Value)) { + $SetContactParams[$Property.Key] = $Property.Value + $HasUpdates = $true + } + } + + # Check if Set-MailContact is needed for additional properties + $MailContactParams = @{ Identity = $ContactIdentity } + $NeedsMailContactUpdate = $false + + if ([bool]$Template.hidefromGAL) { + $MailContactParams.HiddenFromAddressListsEnabled = $true + $NeedsMailContactUpdate = $true + $HasUpdates = $true + } + + if (![string]::IsNullOrWhiteSpace($Template.mailTip)) { + $MailContactParams.MailTip = $Template.mailTip + $NeedsMailContactUpdate = $true + $HasUpdates = $true + } + + # Only increment and update if there are actual changes + if ($HasUpdates) { + $ContactsRequiringUpdates++ + + # Apply Set-Contact updates if needed + if ($SetContactParams.Count -gt 1) { + $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-Contact' -cmdParams $SetContactParams -UseSystemMailbox $true + } + + # Apply Set-MailContact updates if needed + if ($NeedsMailContactUpdate) { + $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-MailContact' -cmdParams $MailContactParams -UseSystemMailbox $true + } + } + } + catch { + $UpdateFailures++ + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API $APIName -tenant $tenant -message "Failed to update additional fields for contact $($Template.displayName): $ErrorMessage" -sev 'Error' + } + } + + # Log update summary only if updates were needed + if ($ContactsRequiringUpdates -gt 0) { + $SuccessfulUpdates = $ContactsRequiringUpdates - $UpdateFailures + Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: Updated additional fields for $SuccessfulUpdates of $ContactsRequiringUpdates contacts" -sev Info + } + } + + # Final summary + if ($ProcessingFailures -gt 0) { + Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: $ProcessingFailures contacts failed to process" -sev Error + } + } + } + + if ($AlertEnabled) { + $MissingContacts = ($CompareList | Where-Object { $_.missing }).Count + $ExistingContacts = ($CompareList | Where-Object { -not $_.missing }).Count + + if ($MissingContacts -gt 0 -or $ExistingContacts -gt 0) { + foreach ($Contact in $CompareList) { + if ($Contact.missing) { + $CurrentInfo = $Contact.Template | Select-Object -Property displayName, email, missing + Write-StandardsAlert -message "Mail contact $($Contact.Template.displayName) from template $($Contact.TemplateGUID) is missing." -object $CurrentInfo -tenant $Tenant -standardName 'DeployContactTemplate' + } + else { + $CurrentInfo = $CurrentContacts | Where-Object -Property DisplayName -eq $Contact.Template.displayName | Select-Object -Property DisplayName, ExternalEmailAddress, FirstName, LastName + Write-StandardsAlert -message "Mail contact $($Contact.Template.displayName) from template $($Contact.TemplateGUID) will be updated to match template." -object $CurrentInfo -tenant $Tenant -standardName 'DeployContactTemplate' + } + } + Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: $MissingContacts missing, $ExistingContacts to update" -sev Info + } else { + Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: No contacts need processing" -sev Info + } + } + + if ($ReportEnabled) { + foreach ($Contact in $CompareList) { + Set-CIPPStandardsCompareField -FieldName "standards.DeployContactTemplate" -FieldValue $Contact.StateIsCorrect -TenantFilter $Tenant + } + } + } + catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API $APIName -tenant $tenant -message "Failed to create or update mail contact(s) from templates, Error: $ErrorMessage" -sev 'Error' + } +} diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContactTemplate b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContactTemplate deleted file mode 100644 index b1f3552b5d36..000000000000 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContactTemplate +++ /dev/null @@ -1,262 +0,0 @@ -function Invoke-CIPPStandardDeployMailContactTemplate { - <# - .FUNCTIONALITY - Internal - .COMPONENT - (APIName) DeployMailContactTemplate - .SYNOPSIS - (Label) Deploy Mail Contact Template - .DESCRIPTION - (Helptext) Creates a new mail contact in Exchange Online across all selected tenants. The contact will be visible in the Global Address List unless hidden. - (DocsDescription) This standard creates a new mail contact in Exchange Online. Mail contacts are useful for adding external email addresses to your organization's address book. They can be used for distribution lists, shared mailboxes, and other collaboration scenarios. - .NOTES - CAT - Exchange Standards - TAG - ADDEDCOMPONENT - {"type":"textField","name":"ExternalEmailAddress","label":"External Email Address","required":true} - {"type":"textField","name":"DisplayName","label":"Display Name","required":true} - {"type":"textField","name":"FirstName","label":"First Name","required":false} - {"type":"textField","name":"LastName","label":"Last Name","required":false} - {"type":"textField","name":"Company","label":"Company","required":false} - {"type":"textField","name":"Office","label":"Office","required":false} - {"type":"textField","name":"State","label":"State","required":false} - {"type":"textField","name":"Phone","label":"Phone Number","required":false} - {"type":"textField","name":"Website","label":"Website","required":false} - {"type":"textField","name":"MailTip","label":"Mail Tip","required":false} - {"type":"switch","name":"HideFromGAL","label":"Hide from Global Address List"} - MULTIPLE - True - IMPACT - Low Impact - ADDEDDATE - 2024-03-19 - POWERSHELLEQUIVALENT - New-MailContact - RECOMMENDEDBY - "CIPP" - UPDATECOMMENTBLOCK - Run the Tools\Update-StandardsComments.ps1 script to update this comment block - .LINK - https://docs.cipp.app/user-documentation/tenant/standards/list-standards - #> - - param($Tenant, $Settings) - - $APIName = 'Standards' - - # Helper function to get value from field (handles both string and {label,value} object) - function Get-FieldValue($field) { - if ($field -is [string]) { - return $field - } elseif ($field.value) { - return $field.value - } else { - return "" - } - } - - try { - Write-LogMessage -API $APIName -tenant $Tenant -message "DeployMailContactTemplate: Processing $($Settings.Count) contact(s)" -sev Info - - # Get the current contacts - $CurrentContacts = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-MailContact' -ErrorAction Stop - - # Compare the settings from standard with the current contacts - $CompareList = foreach ($Contact in $Settings) { - try { - # Extract values using helper function - $displayName = Get-FieldValue $Contact.DisplayName - $emailAddress = Get-FieldValue $Contact.ExternalEmailAddress - - # Input validation for required fields - if ([string]::IsNullOrWhiteSpace($displayName)) { - Write-LogMessage -API $APIName -tenant $Tenant -message "DeployMailContactTemplate: DisplayName cannot be empty for contact." -sev Error - continue - } - - if ([string]::IsNullOrWhiteSpace($emailAddress)) { - Write-LogMessage -API $APIName -tenant $Tenant -message "DeployMailContactTemplate: ExternalEmailAddress cannot be empty for contact $displayName." -sev Error - continue - } - - # Validate email address format - try { - $null = [System.Net.Mail.MailAddress]::new($emailAddress) - } - catch { - Write-LogMessage -API $APIName -tenant $Tenant -message "DeployMailContactTemplate: Invalid email address format: $emailAddress" -sev Error - continue - } - - # Check if the contact already exists - $ExistingContact = $CurrentContacts | Where-Object { $_.ExternalEmailAddress -eq $emailAddress } - - # Create hashtable with desired contact settings - $ContactData = @{ - DisplayName = $displayName - ExternalEmailAddress = $emailAddress - FirstName = Get-FieldValue $Contact.FirstName - LastName = Get-FieldValue $Contact.LastName - Company = Get-FieldValue $Contact.Company - Office = Get-FieldValue $Contact.Office - State = Get-FieldValue $Contact.State - Phone = Get-FieldValue $Contact.Phone - Website = Get-FieldValue $Contact.Website - MailTip = Get-FieldValue $Contact.MailTip - HideFromGAL = [bool]$Contact.HideFromGAL - } - - # If the contact already exists, check if it matches current settings - if ($ExistingContact) { - # For now, we'll consider existing contacts as "correct" - could add comparison logic here later - $StateIsCorrect = $true - $Action = "None" - $Missing = $false - } - else { - # Contact doesn't exist, needs to be created - $StateIsCorrect = $false - $Action = "Create" - $Missing = $true - } - - [PSCustomObject]@{ - missing = $Missing - StateIsCorrect = $StateIsCorrect - Action = $Action - ContactData = $ContactData - remediate = $Contact.remediate - alert = $Contact.alert - report = $Contact.report - } - } - catch { - $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - $Message = "Failed to compare contact $($Contact.DisplayName), Error: $ErrorMessage" - Write-LogMessage -API $APIName -tenant $tenant -message $Message -sev 'Error' - Return $Message - } - } - - # Remediate each contact which is incorrect or missing - If ($true -in $Settings.remediate) { - foreach ($Contact in $CompareList | Where-Object { $_.remediate -EQ $true -and $_.StateIsCorrect -eq $false }) { - try { - $ContactInfo = $Contact.ContactData - - # Parameters for splatting to create contact - $NewContactParams = @{ - displayName = $ContactInfo.DisplayName - name = $ContactInfo.DisplayName - ExternalEmailAddress = $ContactInfo.ExternalEmailAddress - } - - # Add optional name fields if provided - if (![string]::IsNullOrWhiteSpace($ContactInfo.FirstName)) { - $NewContactParams.FirstName = $ContactInfo.FirstName - } - if (![string]::IsNullOrWhiteSpace($ContactInfo.LastName)) { - $NewContactParams.LastName = $ContactInfo.LastName - } - - try { - # Create the mail contact first - $NewContact = New-ExoRequest -tenantid $Tenant -cmdlet 'New-MailContact' -cmdParams $NewContactParams -UseSystemMailbox $true - - # Build SetContactParams efficiently with only provided values - $SetContactParams = @{ - Identity = $NewContact.id - } - - # Helper to add non-empty values for Set-Contact - $PropertyMap = @{ - 'Company' = $ContactInfo.Company - 'StateOrProvince' = $ContactInfo.State - 'Office' = $ContactInfo.Office - 'Phone' = $ContactInfo.Phone - 'WebPage' = $ContactInfo.Website - } - - # Add only non-null/non-empty properties - foreach ($Property in $PropertyMap.GetEnumerator()) { - if (![string]::IsNullOrWhiteSpace($Property.Value)) { - $SetContactParams[$Property.Key] = $Property.Value - } - } - - # Update the contact with additional details only if we have properties to set - if ($SetContactParams.Count -gt 1) { - Start-Sleep -Milliseconds 500 # Ensure the contact is created before updating - $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-Contact' -cmdParams $SetContactParams -UseSystemMailbox $true - } - - # Check if we need to update MailContact properties - $needsMailContactUpdate = $false - $MailContactParams = @{ - Identity = $NewContact.id - } - - # Hide from GAL if requested - if ([bool]$ContactInfo.HideFromGAL) { - $MailContactParams.HiddenFromAddressListsEnabled = $true - $needsMailContactUpdate = $true - } - - # Add MailTip if provided - if (![string]::IsNullOrWhiteSpace($ContactInfo.MailTip)) { - $MailContactParams.MailTip = $ContactInfo.MailTip - $needsMailContactUpdate = $true - } - - # Only call Set-MailContact if we have changes to make - if ($needsMailContactUpdate) { - Start-Sleep -Milliseconds 500 # Ensure the contact is created before updating - $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-MailContact' -cmdParams $MailContactParams -UseSystemMailbox $true - } - - Write-LogMessage -API $APIName -tenant $Tenant -message "$($Contact.Action)d mail contact '$($ContactInfo.DisplayName)' with email '$($ContactInfo.ExternalEmailAddress)'" -sev Info - } - catch { - $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - Write-LogMessage -API $APIName -tenant $tenant -message "Failed to $($Contact.Action) contact $($ContactInfo.DisplayName), Error: $ErrorMessage" -sev 'Error' - } - } - catch { - $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - Write-LogMessage -API $APIName -tenant $tenant -message "Failed to create or update contact $($Contact.ContactData.DisplayName), Error: $ErrorMessage" -sev 'Error' - } - } - } - - if ($true -in $Settings.alert) { - foreach ($Contact in $CompareList | Where-Object -Property alert -EQ $true) { - if ($Contact.StateIsCorrect) { - Write-LogMessage -API $APIName -tenant $Tenant -message "Mail contact $($Contact.ContactData.DisplayName) has the correct configuration." -sev Info - } - else { - if ($Contact.missing) { - $CurrentInfo = $Contact.ContactData | Select-Object -Property DisplayName, ExternalEmailAddress, missing - Write-StandardsAlert -message "Mail contact $($Contact.ContactData.DisplayName) is missing." -object $CurrentInfo -tenant $Tenant -standardName 'DeployMailContact' - Write-LogMessage -API $APIName -tenant $Tenant -message "Mail contact $($Contact.ContactData.DisplayName) is missing." -sev info - } - else { - $CurrentInfo = $CurrentContacts | Where-Object -Property ExternalEmailAddress -eq $Contact.ContactData.ExternalEmailAddress | Select-Object -Property DisplayName, ExternalEmailAddress, FirstName, LastName - Write-StandardsAlert -message "Mail contact $($Contact.ContactData.DisplayName) does not match the expected configuration." -object $CurrentInfo -tenant $Tenant -standardName 'DeployMailContact' - Write-LogMessage -API $APIName -tenant $Tenant -message "Mail contact $($Contact.ContactData.DisplayName) does not match the expected configuration. We've generated an alert" -sev info - } - } - } - } - - if ($true -in $Settings.report) { - foreach ($Contact in $CompareList | Where-Object -Property report -EQ $true) { - Set-CIPPStandardsCompareField -FieldName "standards.DeployMailContact" -FieldValue $Contact.StateIsCorrect -TenantFilter $Tenant - } - } - } - catch { - $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - Write-LogMessage -API $APIName -tenant $tenant -message "Failed to create or update mail contact(s), Error: $ErrorMessage" -sev 'Error' - } -} From f7d5c062d804c02f47439c8af250b3964f20a3a4 Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Sun, 1 Jun 2025 00:22:52 +0800 Subject: [PATCH 016/160] much quicker requests for list all contacts but less detailed --- .../Contacts/Invoke-ListContacts.ps1 | 52 +++---------------- 1 file changed, 7 insertions(+), 45 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-ListContacts.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-ListContacts.ps1 index 34355e55a261..02b9e60c623c 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-ListContacts.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-ListContacts.ps1 @@ -87,7 +87,7 @@ Function Invoke-ListContacts { try { if (![string]::IsNullOrWhiteSpace($ContactID)) { - # Single contact request + # Single contact request - keep existing complex formatting Write-Host "Getting specific contact: $ContactID" $Contact = New-EXORequest -tenantid $TenantFilter -cmdlet 'Get-Contact' -cmdParams @{ @@ -105,55 +105,17 @@ Function Invoke-ListContacts { $ContactResponse = ConvertTo-ContactObject -Contact $Contact -MailContact $MailContact } else { - # Get all contacts + # Get all contacts - simplified approach Write-Host "Getting all contacts" - $Contacts = New-EXORequest -tenantid $TenantFilter -cmdlet 'Get-Contact' -cmdParams @{ - RecipientTypeDetails = 'MailContact' + $ContactResponse = New-EXORequest -tenantid $TenantFilter -cmdlet 'Get-Contact' -cmdParams @{ + Filter = "RecipientTypeDetails -eq 'MailContact'" ResultSize = 'Unlimited' - } + } | Select-Object -Property City, Company, Department, DisplayName, FirstName, LastName, IsDirSynced, Guid, WindowsEmailAddress - # Exit if no contacts - if (!$Contacts -or $Contacts.Count -eq 0) { + # Return empty array if no contacts found + if (!$ContactResponse) { $ContactResponse = @() - } else { - # Filter contacts with missing IDs - $ValidContacts = $Contacts.Where({$_.Id -and $_.Identity}) - - if ($ValidContacts.Count -eq 0) { - $ContactResponse = @() - } else { - $ContactIdentities = $ValidContacts.Identity - $MailContacts = New-EXORequest -tenantid $TenantFilter -cmdlet 'Get-MailContact' -cmdParams @{ - ResultSize = 'Unlimited' - } | Where-Object { $_.Identity -in $ContactIdentities } - - # Build dictionary - $MailContactLookup = [Dictionary[string, object]]::new( - $MailContacts.Count, - [StringComparer]::OrdinalIgnoreCase - ) - - foreach ($mc in $MailContacts) { - if ($mc.Identity) { - $MailContactLookup[$mc.Identity] = $mc - } - } - - $FormattedContacts = [List[object]]::new($ValidContacts.Count) - - # Process contacts - foreach ($Contact in $ValidContacts) { - if ($MailContactLookup.ContainsKey($Contact.Identity)) { - $ContactObj = ConvertTo-ContactObject -Contact $Contact -MailContact $MailContactLookup[$Contact.Identity] - if ($ContactObj) { - $FormattedContacts.Add($ContactObj) - } - } - } - - $ContactResponse = $FormattedContacts.ToArray() - } } } From 1084b48fc745bc02b4cede8e841a939b0f309767 Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Sun, 1 Jun 2025 00:35:39 +0800 Subject: [PATCH 017/160] fixes --- ...oke-CIPPStandardDeployContactTemplates.ps1 | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 index 390ad4b64fe0..692a1eecbc64 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 @@ -203,22 +203,34 @@ function Invoke-CIPPStandardDeployContactTemplates { $Template = $Contact.Template $ExistingContact = $CurrentContacts | Where-Object { $_.DisplayName -eq $Template.displayName } - # Update basic MailContact properties - $UpdateContactParams = @{ + # Update MailContact properties (email address) + $UpdateMailContactParams = @{ Identity = $ExistingContact.Identity ExternalEmailAddress = $Template.email } - # Add optional name fields if provided + # Update the existing mail contact + $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-MailContact' -cmdParams $UpdateMailContactParams -UseSystemMailbox $true + + # Update Contact properties (names) if provided + $UpdateContactParams = @{ + Identity = $ExistingContact.Identity + } + $ContactNeedsUpdate = $false + if (![string]::IsNullOrWhiteSpace($Template.firstName)) { $UpdateContactParams.FirstName = $Template.firstName + $ContactNeedsUpdate = $true } if (![string]::IsNullOrWhiteSpace($Template.lastName)) { $UpdateContactParams.LastName = $Template.lastName + $ContactNeedsUpdate = $true } - # Update the existing mail contact - $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-MailContact' -cmdParams $UpdateContactParams -UseSystemMailbox $true + # Only update Contact if we have name changes + if ($ContactNeedsUpdate) { + $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-Contact' -cmdParams $UpdateContactParams -UseSystemMailbox $true + } # Store contact info for second pass $ProcessedContacts.Add([PSCustomObject]@{ From 5518b8ae2b3a5a07ef10d7641a33ad5737b3a6a9 Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Sat, 31 May 2025 12:14:57 +0800 Subject: [PATCH 018/160] New options for creating, editing, templating and standardising contacts --- .../Contacts/Invoke-AddContact.ps1 | 107 +++++ .../Contacts/Invoke-AddContactTemplates.ps1 | 68 ++++ .../Invoke-DeployContactTemplates.ps1 | 184 +++++++++ .../Contacts/Invoke-EditContact.ps1 | 94 +++++ .../Contacts/Invoke-EditContactTemplates.ps1 | 84 ++++ .../Contacts/Invoke-ListContactTemplates.ps1 | 66 ++++ .../Contacts/Invoke-ListContacts.ps1 | 135 +++++++ .../{ => Contacts}/Invoke-RemoveContact.ps1 | 0 .../Invoke-RemoveContactTemplates.ps1 | 37 ++ .../Administration/Invoke-AddContact.ps1 | 53 --- .../Administration/Invoke-EditContact.ps1 | 62 --- .../Administration/Invoke-ListContacts.ps1 | 71 ---- ...oke-CIPPStandardDeployContactTemplates.ps1 | 374 ++++++++++++++++++ .../Invoke-CIPPStandardDeployMailContact.ps1 | 2 +- ...oke-CIPPStandardMailboxRecipientLimits.ps1 | 6 +- openapi.json | 83 ++++ 16 files changed, 1238 insertions(+), 188 deletions(-) create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-AddContact.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-AddContactTemplates.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-DeployContactTemplates.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-EditContact.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-EditContactTemplates.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-ListContactTemplates.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-ListContacts.ps1 rename Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/{ => Contacts}/Invoke-RemoveContact.ps1 (100%) create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-RemoveContactTemplates.ps1 delete mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-AddContact.ps1 delete mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-EditContact.ps1 delete mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListContacts.ps1 create mode 100644 Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-AddContact.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-AddContact.ps1 new file mode 100644 index 000000000000..2568ccebe9e5 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-AddContact.ps1 @@ -0,0 +1,107 @@ +using namespace System.Net + +Function Invoke-AddContact { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Exchange.Contact.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + + $ContactObject = $Request.Body + $TenantId = $ContactObject.tenantid + + try { + # Prepare the body for New-MailContact cmdlet + $BodyToship = @{ + displayName = $ContactObject.displayName + name = $ContactObject.displayName + ExternalEmailAddress = $ContactObject.email + FirstName = $ContactObject.firstName + LastName = $ContactObject.lastName + } + + # Create the mail contact first + $NewContact = New-ExoRequest -tenantid $TenantId -cmdlet 'New-MailContact' -cmdParams $BodyToship -UseSystemMailbox $true + + # Build SetContactParams efficiently with only provided values + $SetContactParams = @{ + Identity = $NewContact.id + } + + # Helper to add non-empty values + $PropertyMap = @{ + 'Title' = $ContactObject.Title + 'Company' = $ContactObject.Company + 'StreetAddress' = $ContactObject.StreetAddress + 'City' = $ContactObject.City + 'StateOrProvince' = $ContactObject.State + 'PostalCode' = $ContactObject.PostalCode + 'CountryOrRegion' = $ContactObject.CountryOrRegion + 'Phone' = $ContactObject.phone + 'MobilePhone' = $ContactObject.mobilePhone + 'WebPage' = $ContactObject.website + } + + # Add only non-null/non-empty properties + foreach ($Property in $PropertyMap.GetEnumerator()) { + if (![string]::IsNullOrWhiteSpace($Property.Value)) { + $SetContactParams[$Property.Key] = $Property.Value + } + } + + # Update the contact with additional details only if we have properties to set + if ($SetContactParams.Count -gt 1) { + Start-Sleep -Milliseconds 500 # Ensure the contact is created before updating + $null = New-ExoRequest -tenantid $TenantId -cmdlet 'Set-Contact' -cmdParams $SetContactParams -UseSystemMailbox $true + } + + # Check if we need to update MailContact properties + $needsMailContactUpdate = $false + $MailContactParams = @{ + Identity = $NewContact.id + } + + # Only add HiddenFromAddressListsEnabled if we're actually hiding from GAL + if ([bool]$ContactObject.hidefromGAL) { + $MailContactParams.HiddenFromAddressListsEnabled = $true + $needsMailContactUpdate = $true + } + + # Add MailTip if provided + if (![string]::IsNullOrWhiteSpace($ContactObject.mailTip)) { + $MailContactParams.MailTip = $ContactObject.mailTip + $needsMailContactUpdate = $true + } + + # Only call Set-MailContact if we have changes to make + if ($needsMailContactUpdate) { + Start-Sleep -Milliseconds 500 # Ensure the contact is created before updating + $null = New-ExoRequest -tenantid $TenantId -cmdlet 'Set-MailContact' -cmdParams $MailContactParams -UseSystemMailbox $true + } + + # Log the result + $Result = "Successfully created contact $($ContactObject.displayName) with email address $($ContactObject.email)" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantId -message $Result -Sev 'Info' + $StatusCode = [HttpStatusCode]::OK + } + catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to create contact. $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantId -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 = @{Results = $Result } + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-AddContactTemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-AddContactTemplates.ps1 new file mode 100644 index 000000000000..0537e503e980 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-AddContactTemplates.ps1 @@ -0,0 +1,68 @@ +using namespace System.Net + +Function Invoke-AddContactTemplates { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + Exchange.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -Headers $Headers -API $APINAME -message 'Accessed this API' -Sev Debug + Write-Host ($request | ConvertTo-Json -Depth 10 -Compress) + + try { + $GUID = (New-Guid).GUID + + # Create a new ordered hashtable to store selected properties + $contactObject = [ordered]@{} + + # Set name and comments first + $contactObject["name"] = $Request.body.displayName + $contactObject["comments"] = "Contact template created $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" + + # Copy specific properties we want to keep + $propertiesToKeep = @( + "displayName", "firstName", "lastName", "email", "hidefromGAL", "streetAddress", "postalCode", + "city", "state", "country", "companyName", "mobilePhone", "businessPhone", "jobTitle", "website", "mailTip" + ) + + # Copy each property if it exists + foreach ($prop in $propertiesToKeep) { + if ($null -ne $Request.body.$prop) { + $contactObject[$prop] = $Request.body.$prop + } + } + + # Convert to JSON + $JSON = $contactObject | ConvertTo-Json -Depth 10 + + # Save the template to Azure Table Storage + $Table = Get-CippTable -tablename 'templates' + $Table.Force = $true + Add-CIPPAzDataTableEntity @Table -Entity @{ + JSON = "$JSON" + RowKey = "$GUID" + PartitionKey = 'ContactTemplate' + } + + Write-LogMessage -Headers $Headers -API $APINAME -message "Created Contact Template $($contactObject.name) with GUID $GUID" -Sev Info + $body = [pscustomobject]@{'Results' = "Created Contact Template $($contactObject.name) with GUID $GUID" } + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -Headers $Headers -API $APINAME -message "Failed to create Contact template: $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage + $body = [pscustomobject]@{'Results' = "Failed to create Contact template: $($ErrorMessage.NormalizedError)" } + $StatusCode = [HttpStatusCode]::Forbidden + } + + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = $body + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-DeployContactTemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-DeployContactTemplates.ps1 new file mode 100644 index 000000000000..14fa69806c3f --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-DeployContactTemplates.ps1 @@ -0,0 +1,184 @@ +using namespace System.Net + +Function Invoke-DeployContactTemplates { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Exchange.Contact.ReadWrite + .DESCRIPTION + This function deploys contact(s) from template(s) to selected tenants. + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + + try { + $RequestBody = $Request.Body + + # Extract tenant IDs from the selectedTenants objects - get the value property + $SelectedTenants = [System.Collections.Generic.List[string]]::new() + + foreach ($TenantItem in $RequestBody.selectedTenants) { + if ($TenantItem.value) { + $SelectedTenants.Add($TenantItem.value) + } else { + Write-LogMessage -headers $Headers -API $APIName -message "Tenant item missing value property: $($TenantItem | ConvertTo-Json -Compress)" -Sev 'Warning' + } + } + + # Handle AllTenants selection + if ('AllTenants' -in $SelectedTenants) { + $SelectedTenants = [System.Collections.Generic.List[string]]::new() + $AllTenantsList = (Get-Tenants).defaultDomainName + foreach ($Tenant in $AllTenantsList) { + $SelectedTenants.Add($Tenant) + } + } + + # Get the contact templates from TemplateList + $ContactTemplates = [System.Collections.Generic.List[object]]::new() + + if ($RequestBody.TemplateList -and $RequestBody.TemplateList.Count -gt 0) { + # Templates are provided in TemplateList format + foreach ($TemplateItem in $RequestBody.TemplateList) { + if ($TemplateItem.value) { + $ContactTemplates.Add($TemplateItem.value) + } else { + Write-LogMessage -headers $Headers -API $APIName -message "Template item missing value property: $($TemplateItem | ConvertTo-Json -Compress)" -Sev 'Warning' + } + } + } else { + throw "TemplateList is required and must contain at least one template" + } + + if ($ContactTemplates.Count -eq 0) { + throw "No valid contact templates found to deploy" + } + + $Results = foreach ($TenantFilter in $SelectedTenants) { + foreach ($ContactTemplate in $ContactTemplates) { + try { + # Check if contact with this email already exists + $ExistingContactsParam = @{ + tenantid = $TenantFilter + cmdlet = 'Get-MailContact' + cmdParams = @{ + Filter = "ExternalEmailAddress -eq '$($ContactTemplate.email)'" + } + useSystemMailbox = $true + } + + $ExistingContacts = New-ExoRequest @ExistingContactsParam + $ContactExists = $ExistingContacts | Where-Object { $_.ExternalEmailAddress -eq $ContactTemplate.email } + + if ($ContactExists) { + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Contact with email '$($ContactTemplate.email)' already exists in tenant $TenantFilter" -Sev 'Warning' + "Contact '$($ContactTemplate.displayName)' with email '$($ContactTemplate.email)' already exists in tenant $TenantFilter" + continue + } + + # Prepare the body for New-MailContact cmdlet + $BodyToship = @{ + displayName = $ContactTemplate.displayName + name = $ContactTemplate.displayName + ExternalEmailAddress = $ContactTemplate.email + FirstName = $ContactTemplate.firstName + LastName = $ContactTemplate.lastName + } + + # Create the mail contact first + $NewContact = New-ExoRequest -tenantid $TenantFilter -cmdlet 'New-MailContact' -cmdParams $BodyToship -UseSystemMailbox $true + + # Build SetContactParams efficiently with only provided values + $SetContactParams = @{ + Identity = $NewContact.id + } + + # Helper to add non-empty values + $PropertyMap = @{ + 'Title' = $ContactTemplate.jobTitle + 'Company' = $ContactTemplate.companyName + 'StreetAddress' = $ContactTemplate.streetAddress + 'City' = $ContactTemplate.city + 'StateOrProvince' = $ContactTemplate.state + 'PostalCode' = $ContactTemplate.postalCode + 'CountryOrRegion' = $ContactTemplate.country + 'Phone' = $ContactTemplate.businessPhone + 'MobilePhone' = $ContactTemplate.mobilePhone + 'WebPage' = $ContactTemplate.website + } + + # Add only non-null/non-empty properties + foreach ($Property in $PropertyMap.GetEnumerator()) { + if (![string]::IsNullOrWhiteSpace($Property.Value)) { + $SetContactParams[$Property.Key] = $Property.Value + } + } + + # Update the contact with additional details only if we have properties to set + if ($SetContactParams.Count -gt 1) { + Start-Sleep -Milliseconds 500 # Ensure the contact is created before updating + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Set-Contact' -cmdParams $SetContactParams -UseSystemMailbox $true + } + + # Check if we need to update MailContact properties + $needsMailContactUpdate = $false + $MailContactParams = @{ + Identity = $NewContact.id + } + + # Only add HiddenFromAddressListsEnabled if we're actually hiding from GAL + if ([bool]$ContactTemplate.hidefromGAL) { + $MailContactParams.HiddenFromAddressListsEnabled = $true + $needsMailContactUpdate = $true + } + + # Add MailTip if provided + if (![string]::IsNullOrWhiteSpace($ContactTemplate.mailTip)) { + $MailContactParams.MailTip = $ContactTemplate.mailTip + $needsMailContactUpdate = $true + } + + # Only call Set-MailContact if we have changes to make + if ($needsMailContactUpdate) { + Start-Sleep -Milliseconds 500 # Ensure the contact is created before updating + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Set-MailContact' -cmdParams $MailContactParams -UseSystemMailbox $true + } + + # Log the result + $ContactResult = "Successfully created contact '$($ContactTemplate.displayName)' with email '$($ContactTemplate.email)'" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $ContactResult -Sev 'Info' + + # Return success message as a simple string + "Successfully deployed contact '$($ContactTemplate.displayName)' to tenant $TenantFilter" + } + catch { + $ErrorMessage = Get-CippException -Exception $_ + $ErrorDetail = "Failed to deploy contact '$($ContactTemplate.displayName)' to tenant $TenantFilter. Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $ErrorDetail -Sev 'Error' + + # Return error message as a simple string + "Failed to deploy contact '$($ContactTemplate.displayName)' to tenant $TenantFilter. Error: $($ErrorMessage.NormalizedError)" + } + } + } + + $StatusCode = [HttpStatusCode]::OK + } + catch { + $ErrorMessage = Get-CippException -Exception $_ + $Results = "Failed to process contact template deployment request. Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -message $Results -Sev 'Error' + $StatusCode = [HttpStatusCode]::InternalServerError + } + + # 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/Email-Exchange/Administration/Contacts/Invoke-EditContact.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-EditContact.ps1 new file mode 100644 index 000000000000..3b3d458bcba2 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-EditContact.ps1 @@ -0,0 +1,94 @@ +using namespace System.Net + +Function Invoke-EditContact { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Exchange.Contact.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -Headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + + $TenantID = $Request.Body.tenantID + + try { + # Extract contact information from the request body + $contactInfo = $Request.Body + + # Build contact parameters with only provided values + $bodyForSetContact = @{ + Identity = $contactInfo.ContactID + } + + # Map of properties to check and add + $ContactPropertyMap = @{ + 'DisplayName' = $contactInfo.displayName + 'WindowsEmailAddress' = $contactInfo.email + 'FirstName' = $contactInfo.firstName + 'LastName' = $contactInfo.LastName + 'Title' = $contactInfo.Title + 'StreetAddress' = $contactInfo.StreetAddress + 'PostalCode' = $contactInfo.PostalCode + 'City' = $contactInfo.City + 'StateOrProvince' = $contactInfo.State + 'CountryOrRegion' = $contactInfo.CountryOrRegion + 'Company' = $contactInfo.Company + 'MobilePhone' = $contactInfo.mobilePhone + 'Phone' = $contactInfo.phone + 'WebPage' = $contactInfo.website + } + + # Add only non-null/non-empty properties + foreach ($Property in $ContactPropertyMap.GetEnumerator()) { + if (![string]::IsNullOrWhiteSpace($Property.Value)) { + $bodyForSetContact[$Property.Key] = $Property.Value + } + } + + # Update contact only if we have properties to set beyond Identity + if ($bodyForSetContact.Count -gt 1) { + $null = New-ExoRequest -tenantid $TenantID -cmdlet 'Set-Contact' -cmdParams $bodyForSetContact -UseSystemMailbox $true + } + + # Prepare mail contact specific parameters + $MailContactParams = @{ + Identity = $contactInfo.ContactID + } + + # Handle boolean conversion safely + if ($null -ne $contactInfo.hidefromGAL) { + $MailContactParams.HiddenFromAddressListsEnabled = [bool]$contactInfo.hidefromGAL + } + + # Add MailTip if provided + if (![string]::IsNullOrWhiteSpace($contactInfo.mailTip)) { + $MailContactParams.MailTip = $contactInfo.mailTip + } + + # Update mail contact only if we have properties to set beyond Identity + if ($MailContactParams.Count -gt 1) { + $null = New-ExoRequest -tenantid $TenantID -cmdlet 'Set-MailContact' -cmdParams $MailContactParams -UseSystemMailbox $true + } + + $Results = "Successfully edited contact $($contactInfo.displayName)" + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantID -message $Results -Sev Info + $StatusCode = [HttpStatusCode]::OK + } + catch { + $ErrorMessage = Get-CippException -Exception $_ + $Results = "Failed to edit contact. $($ErrorMessage.NormalizedError)" + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantID -message $Results -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 = @{Results = $Results } + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-EditContactTemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-EditContactTemplates.ps1 new file mode 100644 index 000000000000..de2770151466 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-EditContactTemplates.ps1 @@ -0,0 +1,84 @@ +using namespace System.Net + +Function Invoke-EditContactTemplates { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + Exchange.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -Headers $Headers -API $APINAME -message 'Accessed this API' -Sev Debug + Write-Host ($request | ConvertTo-Json -Depth 10 -Compress) + + try { + # Get the ContactTemplateID from the request body + $ContactTemplateID = $Request.body.ContactTemplateID + + if (-not $ContactTemplateID) { + throw "ContactTemplateID is required for editing a template" + } + + # Check if the template exists + $Table = Get-CippTable -tablename 'templates' + $Filter = "PartitionKey eq 'ContactTemplate' and RowKey eq '$ContactTemplateID'" + $ExistingTemplate = Get-CIPPAzDataTableEntity @Table -Filter $Filter + + if (-not $ExistingTemplate) { + throw "Contact template with ID $ContactTemplateID not found" + } + + Write-LogMessage -Headers $Headers -API $APINAME -message "Updating Contact Template with ID: $ContactTemplateID" -Sev Info + + # Create a new ordered hashtable to store selected properties + $contactObject = [ordered]@{} + + # Set name and comments + $contactObject["name"] = $Request.body.displayName + $contactObject["comments"] = "Contact template updated $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" + + # Copy specific properties we want to keep + $propertiesToKeep = @( + "displayName", "firstName", "lastName", "email", "hidefromGAL", "streetAddress", "postalCode", + "city", "state", "country", "companyName", "mobilePhone", "businessPhone", "jobTitle", "website", "mailTip" + ) + + # Copy each property from the request + foreach ($prop in $propertiesToKeep) { + if ($null -ne $Request.body.$prop) { + $contactObject[$prop] = $Request.body.$prop + } + } + + # Convert to JSON + $JSON = $contactObject | ConvertTo-Json -Depth 10 + + # Overwrite the template in Azure Table Storage + $Table = Get-CippTable -tablename 'templates' + $Table.Force = $true + Add-CIPPAzDataTableEntity @Table -Entity @{ + JSON = "$JSON" + RowKey = "$ContactTemplateID" + PartitionKey = 'ContactTemplate' + } + + Write-LogMessage -Headers $Headers -API $APINAME -message "Updated Contact Template $($contactObject.name) with GUID $ContactTemplateID" -Sev Info + $body = [pscustomobject]@{'Results' = "Updated Contact Template $($contactObject.name) with GUID $ContactTemplateID" } + $StatusCode = [HttpStatusCode]::OK + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -Headers $Headers -API $APINAME -message "Failed to update Contact template: $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage + $body = [pscustomobject]@{'Results' = "Failed to update Contact template: $($ErrorMessage.NormalizedError)" } + $StatusCode = [HttpStatusCode]::Forbidden + } + + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = $body + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-ListContactTemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-ListContactTemplates.ps1 new file mode 100644 index 000000000000..7bd3bb63ef74 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-ListContactTemplates.ps1 @@ -0,0 +1,66 @@ +using namespace System.Net +Function Invoke-ListContactTemplates { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + Exchange.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + + $Table = Get-CippTable -tablename 'templates' + $Templates = Get-ChildItem 'Config\*.ContactTemplate.json' | ForEach-Object { + $Entity = @{ + JSON = "$(Get-Content $_)" + RowKey = "$($_.name)" + PartitionKey = 'ContactTemplate' + GUID = "$($_.name)" + } + Add-CIPPAzDataTableEntity @Table -Entity $Entity -Force + } + + # Check if a specific template ID is requested + if ($Request.query.ID -or $Request.query.id) { + $RequestedID = $Request.query.ID ?? $Request.query.id + Write-LogMessage -headers $Headers -API $APIName -message "Retrieving specific template with ID: $RequestedID" -Sev 'Debug' + + # Query directly for the specific template by RowKey for efficiency + $Filter = "PartitionKey eq 'ContactTemplate' and RowKey eq '$RequestedID'" + $Templates = (Get-CIPPAzDataTableEntity @Table -Filter $Filter) | ForEach-Object { + $GUID = $_.RowKey + $data = $_.JSON | ConvertFrom-Json + $data | Add-Member -NotePropertyName 'GUID' -NotePropertyValue $GUID + $data + } + + if (-not $Templates) { + Write-LogMessage -headers $Headers -API $APIName -message "Template with ID $RequestedID not found" -Sev 'Warning' + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::NotFound + Body = @{ Error = "Template with ID $RequestedID not found" } + }) + return + } + } else { + # List all policies if no specific ID requested + Write-LogMessage -headers $Headers -API $APIName -message 'Retrieving all contact templates' -Sev 'Debug' + + $Filter = "PartitionKey eq 'ContactTemplate'" + $Templates = (Get-CIPPAzDataTableEntity @Table -Filter $Filter) | ForEach-Object { + $GUID = $_.RowKey + $data = $_.JSON | ConvertFrom-Json + $data | Add-Member -NotePropertyName 'GUID' -NotePropertyValue $GUID + $data + } + } + + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = @($Templates) + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-ListContacts.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-ListContacts.ps1 new file mode 100644 index 000000000000..02b9e60c623c --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-ListContacts.ps1 @@ -0,0 +1,135 @@ +using namespace System.Net +using namespace System.Collections.Generic +using namespace System.Text.RegularExpressions + +Function Invoke-ListContacts { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Exchange.Contact.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + # Get query parameters + $TenantFilter = $Request.Query.tenantFilter + $ContactID = $Request.Query.id + + # Early validation and exit + if (-not $TenantFilter) { + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = 'tenantFilter is required' + }) + return + } + + # Pre-compiled regex for MailTip cleaning + $script:HtmlTagRegex ??= [regex]::new('<[^>]+>', [RegexOptions]::Compiled) + $script:LineBreakRegex ??= [regex]::new('\\n|\r\n|\r', [RegexOptions]::Compiled) + $script:SmtpPrefixRegex ??= [regex]::new('^SMTP:', [RegexOptions]::Compiled -bor [RegexOptions]::IgnoreCase) + + function ConvertTo-ContactObject { + param($Contact, $MailContact) + + # Early exit if essential data missing + if (!$Contact.Id) { return $null } + + $mailAddress = if ($MailContact.ExternalEmailAddress) { + $script:SmtpPrefixRegex.Replace($MailContact.ExternalEmailAddress, [string]::Empty, 1) + } else { $null } + + $cleanMailTip = if ($MailContact.MailTip -and $MailContact.MailTip.Length -gt 0) { + $cleaned = $script:HtmlTagRegex.Replace($MailContact.MailTip, [string]::Empty) + $cleaned = $script:LineBreakRegex.Replace($cleaned, "`n") + $cleaned.Trim() + } else { $null } + + $phoneCapacity = 0 + if ($Contact.Phone) { $phoneCapacity++ } + if ($Contact.MobilePhone) { $phoneCapacity++ } + + $phones = if ($phoneCapacity -gt 0) { + $phoneList = [List[hashtable]]::new($phoneCapacity) + if ($Contact.Phone) { + $phoneList.Add(@{ type = "business"; number = $Contact.Phone }) + } + if ($Contact.MobilePhone) { + $phoneList.Add(@{ type = "mobile"; number = $Contact.MobilePhone }) + } + $phoneList.ToArray() + } else { @() } + + return @{ + id = $Contact.Id + displayName = $Contact.DisplayName + givenName = $Contact.FirstName + surname = $Contact.LastName + mail = $mailAddress + companyName = $Contact.Company + jobTitle = $Contact.Title + website = $Contact.WebPage + notes = $Contact.Notes + hidefromGAL = $MailContact.HiddenFromAddressListsEnabled + mailTip = $cleanMailTip + onPremisesSyncEnabled = $Contact.IsDirSynced + addresses = @(@{ + street = $Contact.StreetAddress + city = $Contact.City + state = $Contact.StateOrProvince + countryOrRegion = $Contact.CountryOrRegion + postalCode = $Contact.PostalCode + }) + phones = $phones + } + } + + try { + if (![string]::IsNullOrWhiteSpace($ContactID)) { + # Single contact request - keep existing complex formatting + Write-Host "Getting specific contact: $ContactID" + + $Contact = New-EXORequest -tenantid $TenantFilter -cmdlet 'Get-Contact' -cmdParams @{ + Identity = $ContactID + } + + $MailContact = New-EXORequest -tenantid $TenantFilter -cmdlet 'Get-MailContact' -cmdParams @{ + Identity = $ContactID + } + + if (!$Contact -or !$MailContact) { + throw "Contact not found or insufficient permissions" + } + + $ContactResponse = ConvertTo-ContactObject -Contact $Contact -MailContact $MailContact + + } else { + # Get all contacts - simplified approach + Write-Host "Getting all contacts" + + $ContactResponse = New-EXORequest -tenantid $TenantFilter -cmdlet 'Get-Contact' -cmdParams @{ + Filter = "RecipientTypeDetails -eq 'MailContact'" + ResultSize = 'Unlimited' + } | Select-Object -Property City, Company, Department, DisplayName, FirstName, LastName, IsDirSynced, Guid, WindowsEmailAddress + + # Return empty array if no contacts found + if (!$ContactResponse) { + $ContactResponse = @() + } + } + + $StatusCode = [HttpStatusCode]::OK + + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + $StatusCode = [HttpStatusCode]::InternalServerError + $ContactResponse = $ErrorMessage + Write-Host "Error in ListContacts: $ErrorMessage" + } + + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = $ContactResponse + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-RemoveContact.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-RemoveContact.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-RemoveContact.ps1 rename to Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-RemoveContact.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-RemoveContactTemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-RemoveContactTemplates.ps1 new file mode 100644 index 000000000000..137b03e18a9b --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-RemoveContactTemplates.ps1 @@ -0,0 +1,37 @@ +using namespace System.Net + +Function Invoke-RemoveContactTemplates { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + Exchange.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + $APIName = $Request.Params.CIPPEndpoint + $User = $Request.Headers + + Write-LogMessage -Headers $User -API $APINAME -message 'Accessed this API' -Sev 'Debug' + $ID = $request.query.ID ?? $request.body.ID + + try { + $Table = Get-CippTable -tablename 'templates' + $Filter = "PartitionKey eq 'ContactTemplate' and RowKey eq '$id'" + $ClearRow = Get-CIPPAzDataTableEntity @Table -Filter $Filter -Property PartitionKey, RowKey + Remove-AzDataTableEntity -Force @Table -Entity $ClearRow + $Result = "Removed Contact Template with ID $ID." + Write-LogMessage -Headers $User -API $APINAME -message $Result -Sev 'Info' + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to remove Contact template with ID $ID. Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -Headers $User -API $APINAME -message $Result -Sev 'Error' -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::Forbidden + } + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{ Results = $Result } + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-AddContact.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-AddContact.ps1 deleted file mode 100644 index 1e7332d359b9..000000000000 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-AddContact.ps1 +++ /dev/null @@ -1,53 +0,0 @@ -using namespace System.Net - -Function Invoke-AddContact { - <# - .FUNCTIONALITY - Entrypoint - .ROLE - Exchange.Contact.ReadWrite - #> - [CmdletBinding()] - param($Request, $TriggerMetadata) - - $APIName = $Request.Params.CIPPEndpoint - $Headers = $Request.Headers - Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' - - $ContactObject = $Request.Body - $TenantId = $ContactObject.tenantid - - try { - - $BodyToship = [pscustomobject] @{ - displayName = $ContactObject.displayName - name = $ContactObject.displayName - ExternalEmailAddress = $ContactObject.email - FirstName = $ContactObject.firstName - LastName = $ContactObject.lastName - - } - # Create the contact - $NewContact = New-ExoRequest -tenantid $TenantId -cmdlet 'New-MailContact' -cmdParams $BodyToship -UseSystemMailbox $true - $null = New-ExoRequest -tenantid $TenantId -cmdlet 'Set-MailContact' -cmdParams @{Identity = $NewContact.id; HiddenFromAddressListsEnabled = [boolean]$ContactObject.hidefromGAL } -UseSystemMailbox $true - - # Log the result - $Result = "Created contact $($ContactObject.displayName) with email address $($ContactObject.email)" - Write-LogMessage -headers $Headers -API $APIName -tenant $TenantId -message $Result -Sev 'Info' - $StatusCode = [HttpStatusCode]::OK - - } catch { - $ErrorMessage = Get-CippException -Exception $_ - $Result = "Failed to create contact. $($ErrorMessage.NormalizedError)" - Write-LogMessage -headers $Headers -API $APIName -tenant $TenantId -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 = @{Results = $Result } - }) - -} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-EditContact.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-EditContact.ps1 deleted file mode 100644 index d46143ce376c..000000000000 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-EditContact.ps1 +++ /dev/null @@ -1,62 +0,0 @@ -using namespace System.Net - -Function Invoke-EditContact { - <# - .FUNCTIONALITY - Entrypoint - .ROLE - Exchange.Contact.ReadWrite - #> - [CmdletBinding()] - param($Request, $TriggerMetadata) - - $APIName = $Request.Params.CIPPEndpoint - $Headers = $Request.Headers - Write-LogMessage -Headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' - - $TenantID = $Request.Body.tenantID - try { - # Extract contact information from the request body - $contactInfo = $Request.Body - - # Log the received contact object - Write-Host "Received contact object: $($contactInfo | ConvertTo-Json)" - - # Prepare the body for the Set-Contact cmdlet - $bodyForSetContact = [pscustomobject] @{ - 'Identity' = $contactInfo.ContactID - 'DisplayName' = $contactInfo.displayName - 'WindowsEmailAddress' = $contactInfo.email - 'FirstName' = $contactInfo.firstName - 'LastName' = $contactInfo.LastName - 'Title' = $contactInfo.Title - 'StreetAddress' = $contactInfo.StreetAddress - 'PostalCode' = $contactInfo.PostalCode - 'City' = $contactInfo.City - 'CountryOrRegion' = $contactInfo.CountryOrRegion - 'Company' = $contactInfo.Company - 'mobilePhone' = $contactInfo.mobilePhone - 'phone' = $contactInfo.phone - } - - # Call the Set-Contact cmdlet to update the contact - $null = New-ExoRequest -tenantid $TenantID -cmdlet 'Set-Contact' -cmdParams $bodyForSetContact -UseSystemMailbox $true - $null = New-ExoRequest -tenantid $TenantID -cmdlet 'Set-MailContact' -cmdParams @{Identity = $contactInfo.ContactID; HiddenFromAddressListsEnabled = [System.Convert]::ToBoolean($contactInfo.hidefromGAL) } -UseSystemMailbox $true - $Results = "Successfully edited contact $($contactInfo.DisplayName)" - Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantID -message $Results -Sev Info - $StatusCode = [HttpStatusCode]::OK - - } catch { - $ErrorMessage = Get-CippException -Exception $_ - $Results = "Failed to edit contact. $($ErrorMessage.NormalizedError)" - Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantID -message $Results -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 = @{Results = $Results } - }) -} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListContacts.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListContacts.ps1 deleted file mode 100644 index 6fb5562635a4..000000000000 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListContacts.ps1 +++ /dev/null @@ -1,71 +0,0 @@ -using namespace System.Net - -Function Invoke-ListContacts { - <# - .FUNCTIONALITY - Entrypoint - .ROLE - Exchange.Contact.Read - #> - [CmdletBinding()] - param($Request, $TriggerMetadata) - - - # Define fields to retrieve - $selectList = @( - 'id', - 'companyName', - 'displayName', - 'mail', - 'onPremisesSyncEnabled', - 'editURL', - 'givenName', - 'jobTitle', - 'surname', - 'addresses', - 'phones' - ) - - # Get query parameters - $TenantFilter = $Request.Query.tenantFilter - $ContactID = $Request.Query.id - - # Validate required parameters - if (-not $TenantFilter) { - $StatusCode = [HttpStatusCode]::BadRequest - $GraphRequest = 'tenantFilter is required' - Write-Host 'Error: Missing tenantFilter parameter' - } else { - try { - # Construct Graph API URI based on whether an ID is provided - $graphUri = if ([string]::IsNullOrWhiteSpace($ContactID) -eq $false) { - "https://graph.microsoft.com/beta/contacts/$($ContactID)?`$select=$($selectList -join ',')" - } else { - "https://graph.microsoft.com/beta/contacts?`$top=999&`$select=$($selectList -join ',')" - } - - # Make the Graph API request - $GraphRequest = New-GraphGetRequest -uri $graphUri -tenantid $TenantFilter - - if ([string]::IsNullOrWhiteSpace($ContactID) -eq $false) { - $HiddenFromGAL = New-EXORequest -tenantid $TenantFilter -cmdlet 'Get-Recipient' -cmdParams @{RecipientTypeDetails = 'MailContact' } -Select 'HiddenFromAddressListsEnabled,ExternalDirectoryObjectId' | Where-Object { $_.ExternalDirectoryObjectId -eq $ContactID } - $GraphRequest | Add-Member -NotePropertyName 'hidefromGAL' -NotePropertyValue $HiddenFromGAL.HiddenFromAddressListsEnabled - } - # Ensure single result when ID is provided - if ($ContactID -and $GraphRequest -is [array]) { - $GraphRequest = $GraphRequest | Select-Object -First 1 - } - $StatusCode = [HttpStatusCode]::OK - } catch { - $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - $StatusCode = [HttpStatusCode]::InternalServerError - $GraphRequest = $ErrorMessage - } - } - - # Return response - Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ - StatusCode = $StatusCode - Body = @($GraphRequest | Where-Object { $null -ne $_.id }) - }) -} diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 new file mode 100644 index 000000000000..692a1eecbc64 --- /dev/null +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 @@ -0,0 +1,374 @@ +function Invoke-CIPPStandardDeployContactTemplates { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) DeployContactTemplates + .SYNOPSIS + (Label) Deploy Contact Templates + .DESCRIPTION + (Helptext) Creates a new contacts in Exchange Online across all selected tenants from saved contact templates. The contact will be visible in the Global Address List unless hidden. + (DocsDescription) This standard creates new contacts in Exchange Online from saved contact templates. Mail contacts are useful for adding external email addresses to your organization's address book. They can be used for distribution lists, shared mailboxes, and other collaboration scenarios. + .NOTES + CAT + Exchange Standards + TAG + ADDEDCOMPONENT + {"type":"textField","name":"TemplateGUID","label":"Contact Template GUID","required":true} + MULTIPLE + True + IMPACT + Low Impact + ADDEDDATE + 2024-03-19 + POWERSHELLEQUIVALENT + New-MailContact + RECOMMENDEDBY + "CIPP" + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + .LINK + https://docs.cipp.app/user-documentation/tenant/standards/list-standards + #> + + param($Tenant, $Settings) + + $APIName = 'Standards' + + + + # Helper function to get template by GUID + function Get-ContactTemplate($TemplateGUID) { + try { + $Table = Get-CippTable -tablename 'templates' + $Filter = "PartitionKey eq 'ContactTemplate' and RowKey eq '$TemplateGUID'" + $StoredTemplate = Get-CIPPAzDataTableEntity @Table -Filter $Filter + + if (-not $StoredTemplate) { + Write-LogMessage -API $APIName -tenant $Tenant -message "Contact template with GUID $TemplateGUID not found" -sev Error + return $null + } + + return $StoredTemplate.JSON | ConvertFrom-Json + } + catch { + Write-LogMessage -API $APIName -tenant $Tenant -message "Failed to retrieve template $TemplateGUID. Error: $($_.Exception.Message)" -sev Error + return $null + } + } + + + + try { + # Extract control flags from Settings + $RemediateEnabled = [bool]$Settings.remediate + $AlertEnabled = [bool]$Settings.alert + $ReportEnabled = [bool]$Settings.report + + # Get templateIds array + if (-not $Settings.templateIds -or $Settings.templateIds.Count -eq 0) { + Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: No template IDs found in settings" -sev Error + return "No template IDs found in settings" + } + + Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: Processing $($Settings.templateIds.Count) template(s)" -sev Info + + # Get the current contacts + $CurrentContacts = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-MailContact' -ErrorAction Stop + + # Process each template in the templateIds array + $CompareList = foreach ($TemplateItem in $Settings.templateIds) { + try { + # Get the template GUID directly from the value property + $TemplateGUID = $TemplateItem.value + + if ([string]::IsNullOrWhiteSpace($TemplateGUID)) { + Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: TemplateGUID cannot be empty." -sev Error + continue + } + + # Fetch the template from storage + $Template = Get-ContactTemplate -TemplateGUID $TemplateGUID + if (-not $Template) { + continue + } + + # Input validation for required fields + if ([string]::IsNullOrWhiteSpace($Template.displayName)) { + Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: DisplayName cannot be empty for template $TemplateGUID." -sev Error + continue + } + + if ([string]::IsNullOrWhiteSpace($Template.email)) { + Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: ExternalEmailAddress cannot be empty for template $TemplateGUID." -sev Error + continue + } + + # Validate email address format + try { + $null = [System.Net.Mail.MailAddress]::new($Template.email) + } + catch { + Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: Invalid email address format: $($Template.email)" -sev Error + continue + } + + # Check if the contact already exists (using DisplayName as key) + $ExistingContact = $CurrentContacts | Where-Object { $_.DisplayName -eq $Template.displayName } + + # If the contact exists, we'll overwrite it; if not, we'll create it + if ($ExistingContact) { + $StateIsCorrect = $false # Always update existing contacts to match template + $Action = "Update" + $Missing = $false + } + else { + # Contact doesn't exist, needs to be created + $StateIsCorrect = $false + $Action = "Create" + $Missing = $true + } + + [PSCustomObject]@{ + missing = $Missing + StateIsCorrect = $StateIsCorrect + Action = $Action + Template = $Template + TemplateGUID = $TemplateGUID + } + } + catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + $Message = "Failed to process template $TemplateGUID, Error: $ErrorMessage" + Write-LogMessage -API $APIName -tenant $tenant -message $Message -sev 'Error' + Return $Message + } + } + + # Remediate each contact which needs to be created or updated + If ($RemediateEnabled) { + $ContactsToProcess = $CompareList | Where-Object { $_.StateIsCorrect -eq $false } + + if ($ContactsToProcess.Count -gt 0) { + $ContactsToCreate = $ContactsToProcess | Where-Object { $_.Action -eq "Create" } + $ContactsToUpdate = $ContactsToProcess | Where-Object { $_.Action -eq "Update" } + + Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: Processing $($ContactsToCreate.Count) new contacts, $($ContactsToUpdate.Count) existing contacts" -sev Info + + # First pass: Create new mail contacts and update existing ones + $ProcessedContacts = [System.Collections.Generic.List[PSCustomObject]]::new() + $ProcessingFailures = 0 + + # Handle new contacts + foreach ($Contact in $ContactsToCreate) { + try { + $Template = $Contact.Template + + # Parameters for creating new contact + $NewContactParams = @{ + displayName = $Template.displayName + name = $Template.displayName + ExternalEmailAddress = $Template.email + } + + # Add optional name fields if provided + if (![string]::IsNullOrWhiteSpace($Template.firstName)) { + $NewContactParams.FirstName = $Template.firstName + } + if (![string]::IsNullOrWhiteSpace($Template.lastName)) { + $NewContactParams.LastName = $Template.lastName + } + + # Create the mail contact + $NewContact = New-ExoRequest -tenantid $Tenant -cmdlet 'New-MailContact' -cmdParams $NewContactParams -UseSystemMailbox $true + + # Store contact info for second pass + $ProcessedContacts.Add([PSCustomObject]@{ + Contact = $Contact + ContactObject = $NewContact + Template = $Template + IsNew = $true + }) + } + catch { + $ProcessingFailures++ + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API $APIName -tenant $tenant -message "Failed to create contact $($Template.displayName): $ErrorMessage" -sev 'Error' + } + } + + # Handle existing contacts - update their basic properties + foreach ($Contact in $ContactsToUpdate) { + try { + $Template = $Contact.Template + $ExistingContact = $CurrentContacts | Where-Object { $_.DisplayName -eq $Template.displayName } + + # Update MailContact properties (email address) + $UpdateMailContactParams = @{ + Identity = $ExistingContact.Identity + ExternalEmailAddress = $Template.email + } + + # Update the existing mail contact + $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-MailContact' -cmdParams $UpdateMailContactParams -UseSystemMailbox $true + + # Update Contact properties (names) if provided + $UpdateContactParams = @{ + Identity = $ExistingContact.Identity + } + $ContactNeedsUpdate = $false + + if (![string]::IsNullOrWhiteSpace($Template.firstName)) { + $UpdateContactParams.FirstName = $Template.firstName + $ContactNeedsUpdate = $true + } + if (![string]::IsNullOrWhiteSpace($Template.lastName)) { + $UpdateContactParams.LastName = $Template.lastName + $ContactNeedsUpdate = $true + } + + # Only update Contact if we have name changes + if ($ContactNeedsUpdate) { + $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-Contact' -cmdParams $UpdateContactParams -UseSystemMailbox $true + } + + # Store contact info for second pass + $ProcessedContacts.Add([PSCustomObject]@{ + Contact = $Contact + ContactObject = $ExistingContact + Template = $Template + IsNew = $false + }) + } + catch { + $ProcessingFailures++ + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API $APIName -tenant $tenant -message "Failed to update contact $($Template.displayName): $ErrorMessage" -sev 'Error' + } + } + + # Log processing summary + $ProcessedCount = $ProcessedContacts.Count + if ($ProcessedCount -gt 0) { + Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: Successfully processed $ProcessedCount contacts" -sev Info + + # Wait for contacts to propagate before updating additional fields + Start-Sleep -Seconds 1 + + # Second pass: Update contacts with additional fields (only if needed) + $UpdateFailures = 0 + $ContactsRequiringUpdates = 0 + + foreach ($ProcessedContactInfo in $ProcessedContacts) { + try { + $Template = $ProcessedContactInfo.Template + $ContactObject = $ProcessedContactInfo.ContactObject + $HasUpdates = $false + + # Check if Set-Contact is needed + $ContactIdentity = if ($ProcessedContactInfo.IsNew) { $ContactObject.id } else { $ContactObject.Identity } + $SetContactParams = @{ Identity = $ContactIdentity } + $PropertyMap = @{ + 'Company' = $Template.companyName + 'StateOrProvince' = $Template.state + 'Office' = $Template.streetAddress + 'Phone' = $Template.businessPhone + 'WebPage' = $Template.website + 'Title' = $Template.jobTitle + 'City' = $Template.city + 'PostalCode' = $Template.postalCode + 'CountryOrRegion' = $Template.country + 'MobilePhone' = $Template.mobilePhone + } + + foreach ($Property in $PropertyMap.GetEnumerator()) { + if (![string]::IsNullOrWhiteSpace($Property.Value)) { + $SetContactParams[$Property.Key] = $Property.Value + $HasUpdates = $true + } + } + + # Check if Set-MailContact is needed for additional properties + $MailContactParams = @{ Identity = $ContactIdentity } + $NeedsMailContactUpdate = $false + + if ([bool]$Template.hidefromGAL) { + $MailContactParams.HiddenFromAddressListsEnabled = $true + $NeedsMailContactUpdate = $true + $HasUpdates = $true + } + + if (![string]::IsNullOrWhiteSpace($Template.mailTip)) { + $MailContactParams.MailTip = $Template.mailTip + $NeedsMailContactUpdate = $true + $HasUpdates = $true + } + + # Only increment and update if there are actual changes + if ($HasUpdates) { + $ContactsRequiringUpdates++ + + # Apply Set-Contact updates if needed + if ($SetContactParams.Count -gt 1) { + $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-Contact' -cmdParams $SetContactParams -UseSystemMailbox $true + } + + # Apply Set-MailContact updates if needed + if ($NeedsMailContactUpdate) { + $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-MailContact' -cmdParams $MailContactParams -UseSystemMailbox $true + } + } + } + catch { + $UpdateFailures++ + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API $APIName -tenant $tenant -message "Failed to update additional fields for contact $($Template.displayName): $ErrorMessage" -sev 'Error' + } + } + + # Log update summary only if updates were needed + if ($ContactsRequiringUpdates -gt 0) { + $SuccessfulUpdates = $ContactsRequiringUpdates - $UpdateFailures + Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: Updated additional fields for $SuccessfulUpdates of $ContactsRequiringUpdates contacts" -sev Info + } + } + + # Final summary + if ($ProcessingFailures -gt 0) { + Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: $ProcessingFailures contacts failed to process" -sev Error + } + } + } + + if ($AlertEnabled) { + $MissingContacts = ($CompareList | Where-Object { $_.missing }).Count + $ExistingContacts = ($CompareList | Where-Object { -not $_.missing }).Count + + if ($MissingContacts -gt 0 -or $ExistingContacts -gt 0) { + foreach ($Contact in $CompareList) { + if ($Contact.missing) { + $CurrentInfo = $Contact.Template | Select-Object -Property displayName, email, missing + Write-StandardsAlert -message "Mail contact $($Contact.Template.displayName) from template $($Contact.TemplateGUID) is missing." -object $CurrentInfo -tenant $Tenant -standardName 'DeployContactTemplate' + } + else { + $CurrentInfo = $CurrentContacts | Where-Object -Property DisplayName -eq $Contact.Template.displayName | Select-Object -Property DisplayName, ExternalEmailAddress, FirstName, LastName + Write-StandardsAlert -message "Mail contact $($Contact.Template.displayName) from template $($Contact.TemplateGUID) will be updated to match template." -object $CurrentInfo -tenant $Tenant -standardName 'DeployContactTemplate' + } + } + Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: $MissingContacts missing, $ExistingContacts to update" -sev Info + } else { + Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: No contacts need processing" -sev Info + } + } + + if ($ReportEnabled) { + foreach ($Contact in $CompareList) { + Set-CIPPStandardsCompareField -FieldName "standards.DeployContactTemplate" -FieldValue $Contact.StateIsCorrect -TenantFilter $Tenant + } + } + } + catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API $APIName -tenant $tenant -message "Failed to create or update mail contact(s) from templates, Error: $ErrorMessage" -sev 'Error' + } +} diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1 index 1802ac42847a..9a3b212639c9 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1 @@ -100,4 +100,4 @@ function Invoke-CIPPStandardDeployMailContact { Add-CIPPBPAField -FieldName 'DeployMailContact' -FieldValue $ReportData -StoreAs json -Tenant $Tenant Set-CIPPStandardsCompareField -FieldName 'standards.DeployMailContact' -FieldValue $($ExistingContact ? $true : $ReportData) -Tenant $Tenant } -} \ No newline at end of file +} diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1 index fca1c64d74ed..808774259f0f 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1 @@ -23,6 +23,10 @@ function Invoke-CIPPStandardMailboxRecipientLimits { Set-Mailbox -RecipientLimits RECOMMENDEDBY "CIPP" + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + .LINK + https://docs.cipp.app/user-documentation/tenant/standards/list-standards #> param($Tenant, $Settings) @@ -177,4 +181,4 @@ function Invoke-CIPPStandardMailboxRecipientLimits { } Set-CIPPStandardsCompareField -FieldName 'standards.MailboxRecipientLimits' -FieldValue $FieldValue -Tenant $Tenant } -} \ No newline at end of file +} diff --git a/openapi.json b/openapi.json index 7fb2383fa0cc..261f5778fe1b 100644 --- a/openapi.json +++ b/openapi.json @@ -3557,6 +3557,89 @@ } } }, + "/ListContactTemplates": { + "get": { + "description": "List Contact Templates", + "summary": "List Contact Templates", + "tags": ["GET"], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": {} + } + } + } + }, + "description": "Successful operation" + } + } + } + }, + "/RemoveContactTemplates": { + "get": { + "description": "Remove Contact Template", + "summary": "Remove Contact Template", + "tags": ["GET"], + "parameters": [ + { + "required": true, + "schema": { + "type": "string" + }, + "name": "id", + "in": "query" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": {}, + "type": "object" + } + } + }, + "description": "Successful operation" + } + } + } + }, + "/AddContactTemplates": { + "post": { + "description": "Add Contact Template", + "summary": "Add Contact Template", + "tags": ["POST"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {} + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": {}, + "type": "object" + } + } + }, + "description": "Successful operation" + } + } + } + }, "/ListMailboxRules": { "get": { "description": "ListMailboxRules", From 52743fb82bc3531d0c294789f6a6915dbd1f3aa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Sun, 1 Jun 2025 17:22:22 +0200 Subject: [PATCH 019/160] Fix: Casing and error handling --- .../Invoke-CIPPStandardDisableGuests.ps1 | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableGuests.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableGuests.ps1 index 26510a937daf..56ac2349159d 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableGuests.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableGuests.ps1 @@ -40,7 +40,7 @@ function Invoke-CIPPStandardDisableGuests { Where-Object { $_.signInActivity.lastSuccessfulSignInDateTime -le $90Days } $RecentlyReactivatedUsers = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/auditLogs/directoryAudits?`$filter=activityDisplayName eq 'Enable account' and activityDateTime ge $AuditLookup" -scope 'https://graph.microsoft.com/.default' -tenantid $Tenant | - ForEach-Object { $_.targetResources[0].id } | Select-Object -Unique) + ForEach-Object { $_.targetResources[0].id } | Select-Object -Unique) $GraphRequest = $GraphRequest | Where-Object { -not ($RecentlyReactivatedUsers -contains $_.id) } @@ -48,11 +48,11 @@ function Invoke-CIPPStandardDisableGuests { if ($GraphRequest.Count -gt 0) { foreach ($guest in $GraphRequest) { try { - New-GraphPostRequest -type Patch -tenantid $tenant -uri "https://graph.microsoft.com/beta/users/$($guest.id)" -body '{"accountEnabled":"false"}' + $null = New-GraphPostRequest -type Patch -tenantid $tenant -uri "https://graph.microsoft.com/beta/users/$($guest.id)" -body '{"accountEnabled":"false"}' Write-LogMessage -API 'Standards' -tenant $tenant -message "Disabling guest $($guest.UserPrincipalName) ($($guest.id))" -sev Info } catch { - $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to disable guest $($guest.UserPrincipalName) ($($guest.id)): $ErrorMessage" -sev Error + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to disable guest $($guest.UserPrincipalName) ($($guest.id)): $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage } } } else { @@ -70,9 +70,9 @@ function Invoke-CIPPStandardDisableGuests { } } if ($Settings.report -eq $true) { - $filtered = $GraphRequest | Select-Object -Property UserPrincipalName, id, signInActivity, mail, userType, accountEnabled - $state = $filtered ? $filtered : $true - Set-CIPPStandardsCompareField -FieldName 'standards.DisableGuests' -FieldValue $state -TenantFilter $Tenant - Add-CIPPBPAField -FieldName 'DisableGuests' -FieldValue $filtered -StoreAs json -Tenant $tenant + $Filtered = $GraphRequest | Select-Object -Property UserPrincipalName, id, signInActivity, mail, userType, accountEnabled + $State = $Filtered ? $Filtered : $true + Set-CIPPStandardsCompareField -FieldName 'standards.DisableGuests' -FieldValue $State -TenantFilter $Tenant + Add-CIPPBPAField -FieldName 'DisableGuests' -FieldValue $Filtered -StoreAs json -Tenant $tenant } } From 3067656810e10f7f4b512d6eb26bdeefc1a31ffb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Sun, 1 Jun 2025 18:14:54 +0200 Subject: [PATCH 020/160] Fix: Update terminology from AAD to Entra in DisableSharedMailbox function, change some casing and remove unneeded scope --- ...nvoke-CIPPStandardDisableSharedMailbox.ps1 | 28 +++++++++---------- ...oke-CIPPStandardMailboxRecipientLimits.ps1 | 6 +++- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharedMailbox.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharedMailbox.ps1 index 66bc9dc3e30f..257229cb8aea 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharedMailbox.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharedMailbox.ps1 @@ -5,7 +5,7 @@ function Invoke-CIPPStandardDisableSharedMailbox { .COMPONENT (APIName) DisableSharedMailbox .SYNOPSIS - (Label) Disable Shared Mailbox AAD accounts + (Label) Disable Shared Mailbox Entra accounts .DESCRIPTION (Helptext) Blocks login for all accounts that are marked as a shared mailbox. This is Microsoft best practice to prevent direct logons to shared mailboxes. (DocsDescription) Shared mailboxes can be directly logged into if the password is reset, this presents a security risk as do all shared login credentials. Microsoft's recommendation is to disable the user account for shared mailboxes. It would be a good idea to review the sign-in reports to establish potential impact. @@ -33,39 +33,39 @@ function Invoke-CIPPStandardDisableSharedMailbox { param($Tenant, $Settings) ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'DisableSharedMailbox' - $UserList = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/users?$top=999&$filter=accountEnabled eq true and onPremisesSyncEnabled ne true&$count=true' -Tenantid $tenant -scope 'https://graph.microsoft.com/.default' -ComplexFilter - $SharedMailboxList = (New-GraphGetRequest -uri "https://outlook.office365.com/adminapi/beta/$($Tenant)/Mailbox" -Tenantid $tenant -scope ExchangeOnline | Where-Object { $_.RecipientTypeDetails -EQ 'SharedMailbox' -or $_.RecipientTypeDetails -eq 'SchedulingMailbox' -and $_.UserPrincipalName -in $UserList.UserPrincipalName }) + $UserList = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/users?$top=999&$filter=accountEnabled eq true and onPremisesSyncEnabled ne true&$count=true' -Tenantid $Tenant -ComplexFilter + $SharedMailboxList = (New-GraphGetRequest -uri "https://outlook.office365.com/adminapi/beta/$($Tenant)/Mailbox" -Tenantid $Tenant -scope ExchangeOnline | Where-Object { $_.RecipientTypeDetails -EQ 'SharedMailbox' -or $_.RecipientTypeDetails -eq 'SchedulingMailbox' -and $_.UserPrincipalName -in $UserList.UserPrincipalName }) If ($Settings.remediate -eq $true) { if ($SharedMailboxList) { $SharedMailboxList | ForEach-Object { try { - New-GraphPOSTRequest -uri "https://graph.microsoft.com/v1.0/users/$($_.ObjectKey)" -type PATCH -body '{"accountEnabled":"false"}' -tenantid $tenant - Write-LogMessage -API 'Standards' -tenant $tenant -message "AAD account for shared mailbox $($_.DisplayName) disabled." -sev Info + New-GraphPOSTRequest -uri "https://graph.microsoft.com/v1.0/users/$($_.ObjectKey)" -type PATCH -body '{"accountEnabled":"false"}' -tenantid $Tenant + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Entra account for shared mailbox $($_.DisplayName) disabled." -sev Info } catch { - $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to disable AAD account for shared mailbox. Error: $ErrorMessage" -sev Error + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to disable Entra account for shared mailbox. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage } } } else { - Write-LogMessage -API 'Standards' -tenant $tenant -message 'All AAD accounts for shared mailboxes are already disabled.' -sev Info + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'All Entra accounts for shared mailboxes are already disabled.' -sev Info } } if ($Settings.alert -eq $true) { if ($SharedMailboxList) { - Write-StandardsAlert -message "Shared mailboxes with enabled accounts: $($SharedMailboxList.Count)" -object $SharedMailboxList -tenant $tenant -standardName 'DisableSharedMailbox' -standardId $Settings.standardId - Write-LogMessage -API 'Standards' -tenant $tenant -message "Shared mailboxes with enabled accounts: $($SharedMailboxList.Count)" -sev Info + Write-StandardsAlert -message "Shared mailboxes with enabled accounts: $($SharedMailboxList.Count)" -object $SharedMailboxList -tenant $Tenant -standardName 'DisableSharedMailbox' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Shared mailboxes with enabled accounts: $($SharedMailboxList.Count)" -sev Info } else { - Write-LogMessage -API 'Standards' -tenant $tenant -message 'All AAD accounts for shared mailboxes are disabled.' -sev Info + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'All Entra accounts for shared mailboxes are disabled.' -sev Info } } if ($Settings.report -eq $true) { - $state = $SharedMailboxList ? $SharedMailboxList : $true - Set-CIPPStandardsCompareField -FieldName 'standards.DisableSharedMailbox' -FieldValue $state -Tenant $tenant - Add-CIPPBPAField -FieldName 'DisableSharedMailbox' -FieldValue $SharedMailboxList -StoreAs json -Tenant $tenant + $State = $SharedMailboxList ? $SharedMailboxList : $true + Set-CIPPStandardsCompareField -FieldName 'standards.DisableSharedMailbox' -FieldValue $State -Tenant $Tenant + Add-CIPPBPAField -FieldName 'DisableSharedMailbox' -FieldValue $SharedMailboxList -StoreAs json -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1 index fca1c64d74ed..808774259f0f 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1 @@ -23,6 +23,10 @@ function Invoke-CIPPStandardMailboxRecipientLimits { Set-Mailbox -RecipientLimits RECOMMENDEDBY "CIPP" + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + .LINK + https://docs.cipp.app/user-documentation/tenant/standards/list-standards #> param($Tenant, $Settings) @@ -177,4 +181,4 @@ function Invoke-CIPPStandardMailboxRecipientLimits { } Set-CIPPStandardsCompareField -FieldName 'standards.MailboxRecipientLimits' -FieldValue $FieldValue -Tenant $Tenant } -} \ No newline at end of file +} From 8e5f431ad3f85d1e45bbee7bf2d8335dfad8cbf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Sun, 1 Jun 2025 18:28:52 +0200 Subject: [PATCH 021/160] Feat: New standard to disable unlicensed resource mailbox Entra accounts --- ...oke-CIPPStandardDisableResourceMailbox.ps1 | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 new file mode 100644 index 000000000000..dca3fceecf40 --- /dev/null +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 @@ -0,0 +1,78 @@ +function Invoke-CIPPStandardDisableResourceMailbox { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) DisableResourceMailbox + .SYNOPSIS + (Label) Disable Unlicensed Resource Mailbox Entra accounts + .DESCRIPTION + (Helptext) Blocks login for all accounts that are marked as a resource mailbox and does not have a license assigned. Accounts that are synced from on-premises AD are excluded, as account state is managed in the on-premises AD. + (DocsDescription) Resource mailboxes can be directly logged into if the password is reset, this presents a security risk as do all shared login credentials. Microsoft's recommendation is to disable the user account for resource mailboxes. Accounts that are synced from on-premises AD are excluded, as account state is managed in the on-premises AD. + .NOTES + CAT + Exchange Standards + TAG + "CIS" + ADDEDCOMPONENT + IMPACT + Medium Impact + ADDEDDATE + 2025-06-01 + POWERSHELLEQUIVALENT + Get-Mailbox & Update-MgUser + RECOMMENDEDBY + "CIS" + "CIPP" + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + .LINK + https://docs.cipp.app/user-documentation/tenant/standards/list-standards + #> + + param($Tenant, $Settings) + ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'DisableResourceMailbox' + + # Get all users that are able to be + $UserList = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/users?$top=999&$filter=accountEnabled eq true and onPremisesSyncEnabled ne true and assignedLicenses/$count eq 0&$count=true' -Tenantid $Tenant -ComplexFilter | + Where-Object { $_.userType -eq 'Member' } + $ResourceMailboxList = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-Mailbox' -cmdParams @{ Filter = "RecipientTypeDetails -eq 'RoomMailbox' -or RecipientTypeDetails -eq 'EquipmentMailbox'" } -Select 'UserPrincipalName,DisplayName,RecipientTypeDetails,ExternalDirectoryObjectId' | + Where-Object { $_.ExternalDirectoryObjectId -in $UserList.id } + + If ($Settings.remediate -eq $true) { + Write-Host 'Time to remediate' + + + if ($ResourceMailboxList) { + Write-Host "Resource Mailboxes to disable: $($ResourceMailboxList.Count)" + $ResourceMailboxList | ForEach-Object { + try { + New-GraphPOSTRequest -uri "https://graph.microsoft.com/v1.0/users/$($_.ExternalDirectoryObjectId)" -type PATCH -body '{"accountEnabled":"false"}' -tenantid $Tenant + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Entra account for $($_.RecipientTypeDetails), $($_.DisplayName), $($_.UserPrincipalName) disabled." -sev Info + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to disable Entra account for $($_.RecipientTypeDetails), $($_.DisplayName), $($_.UserPrincipalName). Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + } + } + } else { + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'All Entra accounts for resource mailboxes are already disabled.' -sev Info + } + } + + if ($Settings.alert -eq $true) { + + if ($ResourceMailboxList) { + Write-StandardsAlert -message "Resource mailboxes with enabled accounts: $($ResourceMailboxList.Count)" -object $ResourceMailboxList -tenant $Tenant -standardName 'DisableResourceMailbox' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Resource mailboxes with enabled accounts: $($ResourceMailboxList.Count)" -sev Info + } else { + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'All Entra accounts for resource mailboxes are disabled.' -sev Info + } + } + + if ($Settings.report -eq $true) { + # If there are no resource mailboxes, we set the state to true, so that the standard reports as compliant. + $State = $ResourceMailboxList ? $ResourceMailboxList : $true + Set-CIPPStandardsCompareField -FieldName 'standards.DisableResourceMailbox' -FieldValue $State -Tenant $Tenant + Add-CIPPBPAField -FieldName 'DisableResourceMailbox' -FieldValue $ResourceMailboxList -StoreAs json -Tenant $Tenant + } +} From b8849781ebb0001e217be049367b784c90970bf3 Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 3 Jun 2025 01:35:56 +0800 Subject: [PATCH 022/160] Round 1 --- Config/standards.json | 29 + .../Reports/Invoke-ListSafeLinksFilters.ps1 | 31 - .../Spamfilter/Invoke-EditSafeLinksFilter.ps1 | 57 -- .../Invoke-AddSafeLinksPolicyFromTemplate.ps1 | 237 ++++++ .../Invoke-AddSafeLinksPolicyTemplate.ps1 | 96 +++ .../Invoke-CreateSafeLinksPolicyTemplate.ps1 | 77 ++ .../Invoke-EditSafeLinksPolicy.ps1 | 217 ++++++ .../Invoke-EditSafeLinksPolicyTemplate.ps1 | 89 +++ .../Invoke-ExecDeleteSafeLinksPolicy.ps1 | 94 +++ .../Invoke-ExecNewSafeLinksPolicy.ps1 | 237 ++++++ .../Invoke-ListSafeLinksPolicy.ps1 | 94 +++ .../Invoke-ListSafeLinksPolicyDetails.ps1 | 106 +++ ...oke-ListSafeLinksPolicyTemplateDetails.ps1 | 57 ++ .../Invoke-ListSafeLinksPolicyTemplates.ps1 | 39 + .../Invoke-RemoveSafeLinksPolicyTemplate.ps1 | 35 + ...ke-CIPPStandardSafeLinksTemplatePolicy.ps1 | 538 ++++++++++++++ openapi.json | 674 ++++++++++++++++++ 17 files changed, 2619 insertions(+), 88 deletions(-) delete mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListSafeLinksFilters.ps1 delete mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-EditSafeLinksFilter.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyFromTemplate.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyTemplate.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-CreateSafeLinksPolicyTemplate.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-EditSafeLinksPolicy.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-EditSafeLinksPolicyTemplate.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecDeleteSafeLinksPolicy.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecNewSafeLinksPolicy.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicy.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicyDetails.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicyTemplateDetails.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicyTemplates.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-RemoveSafeLinksPolicyTemplate.ps1 create mode 100644 Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksTemplatePolicy.ps1 diff --git a/Config/standards.json b/Config/standards.json index 92ef2e2806f5..0be93153192d 100644 --- a/Config/standards.json +++ b/Config/standards.json @@ -1722,6 +1722,35 @@ "powershellEquivalent": "New-ProtectionAlert and Set-ProtectionAlert", "recommendedBy": [] }, + { + "name": "standards.SafeLinksTemplatePolicy", + "label": "SafeLinks Policy Template", + "cat": "Templates", + "multiple": false, + "disabledFeatures": { + "report": false, + "warn": false, + "remediate": false + }, + "impact": "Medium Impact", + "addedDate": "2025-04-29", + "helpText": "Deploy and manage SafeLinks policy templates to protect against malicious URLs in emails and Office documents.", + "addedComponent": [ + { + "type": "autoComplete", + "multiple": true, + "creatable": false, + "name": "standards.SafeLinksTemplatePolicy.TemplateIds", + "label": "Select SafeLinks Policy Templates", + "api": { + "url": "/api/ListSafeLinksPolicyTemplates", + "labelField": "name", + "valueField": "GUID", + "queryKey": "ListSafeLinksPolicyTemplates" + } + } + ] + }, { "name": "standards.SafeLinksPolicy", "cat": "Defender Standards", diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListSafeLinksFilters.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListSafeLinksFilters.ps1 deleted file mode 100644 index c9e395c05de7..000000000000 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListSafeLinksFilters.ps1 +++ /dev/null @@ -1,31 +0,0 @@ -function Invoke-ListSafeLinksFilters { - <# - .FUNCTIONALITY - Entrypoint - .ROLE - Exchange.SpamFilter.Read - #> - [CmdletBinding()] - param($Request, $TriggerMetadata) - - $APIName = $Request.Params.CIPPEndpoint - $Headers = $Request.Headers - Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' - - # Interact with query parameters or the body of the request. - $TenantFilter = $Request.Query.TenantFilter - $Policies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-SafeLinksPolicy' | Select-Object -Property * - $Rules = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-SafeLinksRule' | Select-Object -Property * - - $Output = $Policies | Select-Object -Property *, - @{ Name = 'RuleName'; Expression = { foreach ($item in $Rules) { if ($item.SafeLinksPolicy -eq $_.Name) { $item.Name } } } }, - @{ Name = 'Priority'; Expression = { foreach ($item in $Rules) { if ($item.SafeLinksPolicy -eq $_.Name) { $item.Priority } } } }, - @{ Name = 'RecipientDomainIs'; Expression = { foreach ($item in $Rules) { if ($item.SafeLinksPolicy -eq $_.Name) { $item.RecipientDomainIs } } } }, - @{ Name = 'State'; Expression = { foreach ($item in $Rules) { if ($item.SafeLinksPolicy -eq $_.Name) { $item.State } } } } - - # Associate values to output bindings by calling 'Push-OutputBinding'. - Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ - StatusCode = [HttpStatusCode]::OK - Body = $Output - }) -} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-EditSafeLinksFilter.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-EditSafeLinksFilter.ps1 deleted file mode 100644 index fd7b11144b48..000000000000 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-EditSafeLinksFilter.ps1 +++ /dev/null @@ -1,57 +0,0 @@ -function Invoke-EditSafeLinksFilter { - <# - .FUNCTIONALITY - Entrypoint - .ROLE - Exchange.SpamFilter.Read - #> - [CmdletBinding()] - param($Request, $TriggerMetadata) - - $APIName = $Request.Params.CIPPEndpoint - $Headers = $Request.Headers - Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' - - # Interact with query parameters or the body of the request. - $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter - $RuleName = $Request.Query.RuleName ?? $Request.Body.RuleName - $State = $Request.Query.State ?? $Request.Body.State - - try { - $ExoRequestParam = @{ - tenantid = $TenantFilter - cmdParams = @{ - Identity = $RuleName - } - useSystemMailbox = $true - } - - switch ($State) { - 'Enable' { - $ExoRequestParam.Add('cmdlet', 'Enable-SafeLinksRule') - } - 'Disable' { - $ExoRequestParam.Add('cmdlet', 'Disable-SafeLinksRule') - } - Default { - throw 'Invalid state' - } - } - $null = New-ExoRequest @ExoRequestParam - - $Result = "Successfully set SafeLinks rule $($RuleName) to $($State)" - Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Info - $StatusCode = [HttpStatusCode]::OK - } catch { - $ErrorMessage = Get-CippException -Exception $_ - $Result = "Failed setting SafeLinks rule $($RuleName) to $($State). Error: $($ErrorMessage.NormalizedError)" - Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev 'Error' - $StatusCode = [HttpStatusCode]::InternalServerError - } - - # Associate values to output bindings by calling 'Push-OutputBinding'. - Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ - StatusCode = $StatusCode - Body = @{Results = $Result } - }) -} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyFromTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyFromTemplate.ps1 new file mode 100644 index 000000000000..e6e1c90824fc --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyFromTemplate.ps1 @@ -0,0 +1,237 @@ +using namespace System.Net + +Function Invoke-AddSafeLinksPolicyFromTemplate { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Exchange.SafeLinks.ReadWrite + .DESCRIPTION + This function deploys a SafeLinks policy and rule from a template to selected tenants. + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + + try { + $RequestBody = $Request.Body + + # Extract tenant IDs from the selectedTenants objects - just get the value property + $SelectedTenants = $RequestBody.selectedTenants | ForEach-Object { $_.value } + if ('AllTenants' -in $SelectedTenants) { $SelectedTenants = (Get-Tenants).defaultDomainName } + + # Parse the PolicyConfig if it's a string + if ($RequestBody.PolicyConfig -is [string]) { + $PolicyConfig = $RequestBody.PolicyConfig | ConvertFrom-Json -ErrorAction Stop + } else { + $PolicyConfig = $RequestBody.PolicyConfig + } + + # Helper function to process array fields + function Process-ArrayField { + param ( + [Parameter(Mandatory = $false)] + $Field + ) + + if ($null -eq $Field) { return @() } + + # If already an array, process each item + if ($Field -is [array]) { + $result = @() + foreach ($item in $Field) { + if ($item -is [string]) { + $result += $item + } + elseif ($item -is [hashtable] -or $item -is [PSCustomObject]) { + # Extract value from object + if ($null -ne $item.value) { + $result += $item.value + } + elseif ($null -ne $item.userPrincipalName) { + $result += $item.userPrincipalName + } + elseif ($null -ne $item.id) { + $result += $item.id + } + else { + $result += $item.ToString() + } + } + else { + $result += $item.ToString() + } + } + return $result + } + + # If it's a single object + if ($Field -is [hashtable] -or $Field -is [PSCustomObject]) { + if ($null -ne $Field.value) { return @($Field.value) } + if ($null -ne $Field.userPrincipalName) { return @($Field.userPrincipalName) } + if ($null -ne $Field.id) { return @($Field.id) } + } + + # If it's a string, return as an array with one item + if ($Field -is [string]) { + return @($Field) + } + + return @($Field) + } + + $Results = foreach ($TenantFilter in $SelectedTenants) { + try { + # Extract policy name from template + $PolicyName = $PolicyConfig.PolicyName ?? $PolicyConfig.Name + $RuleName = $PolicyConfig.RuleName ?? $PolicyName + + # Check if policy exists by listing all policies and filtering + $ExistingPoliciesParam = @{ + tenantid = $TenantFilter + cmdlet = 'Get-SafeLinksPolicy' + useSystemMailbox = $true + } + + $ExistingPolicies = New-ExoRequest @ExistingPoliciesParam + $PolicyExists = $ExistingPolicies | Where-Object { $_.Name -eq $PolicyName } + + if ($PolicyExists) { + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Policy with name '$PolicyName' already exists in tenant $TenantFilter" -Sev 'Warning' + "Policy with name '$PolicyName' already exists in tenant $TenantFilter" + continue + } + + # Check if rule exists by listing all rules and filtering + $ExistingRulesParam = @{ + tenantid = $TenantFilter + cmdlet = 'Get-SafeLinksRule' + useSystemMailbox = $true + } + + $ExistingRules = New-ExoRequest @ExistingRulesParam + $RuleExists = $ExistingRules | Where-Object { $_.Name -eq $RuleName } + + if ($RuleExists) { + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Rule with name '$RuleName' already exists in tenant $TenantFilter" -Sev 'Warning' + "Rule with name '$RuleName' already exists in tenant $TenantFilter" + continue + } + + # Process arrays in the template + $DoNotRewriteUrls = Process-ArrayField -Field $PolicyConfig.DoNotRewriteUrls + $SentTo = Process-ArrayField -Field $PolicyConfig.SentTo + $SentToMemberOf = Process-ArrayField -Field $PolicyConfig.SentToMemberOf + $RecipientDomainIs = Process-ArrayField -Field $PolicyConfig.RecipientDomainIs + $ExceptIfSentTo = Process-ArrayField -Field $PolicyConfig.ExceptIfSentTo + $ExceptIfSentToMemberOf = Process-ArrayField -Field $PolicyConfig.ExceptIfSentToMemberOf + $ExceptIfRecipientDomainIs = Process-ArrayField -Field $PolicyConfig.ExceptIfRecipientDomainIs + + # PART 1: Create SafeLinks Policy + # Build command parameters for policy + $policyParams = @{ + Name = $PolicyName + } + + # Only add parameters that are explicitly provided in the template + if ($null -ne $PolicyConfig.EnableSafeLinksForEmail) { $policyParams.Add('EnableSafeLinksForEmail', $PolicyConfig.EnableSafeLinksForEmail) } + if ($null -ne $PolicyConfig.EnableSafeLinksForTeams) { $policyParams.Add('EnableSafeLinksForTeams', $PolicyConfig.EnableSafeLinksForTeams) } + if ($null -ne $PolicyConfig.EnableSafeLinksForOffice) { $policyParams.Add('EnableSafeLinksForOffice', $PolicyConfig.EnableSafeLinksForOffice) } + if ($null -ne $PolicyConfig.TrackClicks) { $policyParams.Add('TrackClicks', $PolicyConfig.TrackClicks) } + if ($null -ne $PolicyConfig.AllowClickThrough) { $policyParams.Add('AllowClickThrough', $PolicyConfig.AllowClickThrough) } + if ($null -ne $PolicyConfig.ScanUrls) { $policyParams.Add('ScanUrls', $PolicyConfig.ScanUrls) } + if ($null -ne $PolicyConfig.EnableForInternalSenders) { $policyParams.Add('EnableForInternalSenders', $PolicyConfig.EnableForInternalSenders) } + if ($null -ne $PolicyConfig.DeliverMessageAfterScan) { $policyParams.Add('DeliverMessageAfterScan', $PolicyConfig.DeliverMessageAfterScan) } + if ($null -ne $PolicyConfig.DisableUrlRewrite) { $policyParams.Add('DisableUrlRewrite', $PolicyConfig.DisableUrlRewrite) } + if ($null -ne $DoNotRewriteUrls -and $DoNotRewriteUrls.Count -gt 0) { $policyParams.Add('DoNotRewriteUrls', $DoNotRewriteUrls) } + if ($null -ne $PolicyConfig.AdminDisplayName) { $policyParams.Add('AdminDisplayName', $PolicyConfig.AdminDisplayName) } + if ($null -ne $PolicyConfig.CustomNotificationText) { $policyParams.Add('CustomNotificationText', $PolicyConfig.CustomNotificationText) } + if ($null -ne $PolicyConfig.EnableOrganizationBranding) { $policyParams.Add('EnableOrganizationBranding', $PolicyConfig.EnableOrganizationBranding) } + + $ExoPolicyRequestParam = @{ + tenantid = $TenantFilter + cmdlet = 'New-SafeLinksPolicy' + cmdParams = $policyParams + useSystemMailbox = $true + } + + $null = New-ExoRequest @ExoPolicyRequestParam + $PolicyResult = "Successfully created new SafeLinks policy '$PolicyName'" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $PolicyResult -Sev 'Info' + + # PART 2: Create SafeLinks Rule + # Build command parameters for rule + $ruleParams = @{ + Name = $RuleName + SafeLinksPolicy = $PolicyName + } + + # Only add parameters that are explicitly provided + if ($null -ne $PolicyConfig.Priority) { $ruleParams.Add('Priority', $PolicyConfig.Priority) } + if ($null -ne $PolicyConfig.Description) { $ruleParams.Add('Comments', $PolicyConfig.Description) } + if ($null -ne $SentTo -and $SentTo.Count -gt 0) { $ruleParams.Add('SentTo', $SentTo) } + if ($null -ne $SentToMemberOf -and $SentToMemberOf.Count -gt 0) { $ruleParams.Add('SentToMemberOf', $SentToMemberOf) } + if ($null -ne $RecipientDomainIs -and $RecipientDomainIs.Count -gt 0) { $ruleParams.Add('RecipientDomainIs', $RecipientDomainIs) } + if ($null -ne $ExceptIfSentTo -and $ExceptIfSentTo.Count -gt 0) { $ruleParams.Add('ExceptIfSentTo', $ExceptIfSentTo) } + if ($null -ne $ExceptIfSentToMemberOf -and $ExceptIfSentToMemberOf.Count -gt 0) { $ruleParams.Add('ExceptIfSentToMemberOf', $ExceptIfSentToMemberOf) } + if ($null -ne $ExceptIfRecipientDomainIs -and $ExceptIfRecipientDomainIs.Count -gt 0) { $ruleParams.Add('ExceptIfRecipientDomainIs', $ExceptIfRecipientDomainIs) } + + $ExoRuleRequestParam = @{ + tenantid = $TenantFilter + cmdlet = 'New-SafeLinksRule' + cmdParams = $ruleParams + useSystemMailbox = $true + } + + $null = New-ExoRequest @ExoRuleRequestParam + $RuleResult = "Successfully created new SafeLinks rule '$RuleName'" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $RuleResult -Sev 'Info' + + # If State is specified in the template, enable or disable the rule + if ($null -ne $PolicyConfig.State) { + $Enabled = $PolicyConfig.State -eq "Enabled" + $EnableCmdlet = $Enabled ? 'Enable-SafeLinksRule' : 'Disable-SafeLinksRule' + $EnableRequestParam = @{ + tenantid = $TenantFilter + cmdlet = $EnableCmdlet + cmdParams = @{ + Identity = $RuleName + } + useSystemMailbox = $true + } + + $null = New-ExoRequest @EnableRequestParam + $StateMsg = $Enabled ? "enabled" : "disabled" + } + + # Return success message as a simple string + "Successfully deployed SafeLinks policy '$PolicyName' and rule '$RuleName' to tenant $TenantFilter" + $(if ($null -ne $PolicyConfig.State) { " (rule $StateMsg)" } else { "" }) + } + catch { + $ErrorMessage = Get-CippException -Exception $_ + $ErrorDetail = "Failed to deploy SafeLinks policy template to tenant $TenantFilter. Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $ErrorDetail -Sev 'Error' + + # Return error message as a simple string + "Failed to deploy SafeLinks policy template to tenant $TenantFilter. Error: $($ErrorMessage.NormalizedError)" + } + } + + $StatusCode = [HttpStatusCode]::OK + } + catch { + $ErrorMessage = Get-CippException -Exception $_ + $Results = "Failed to process template deployment request. Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -message $Results -Sev 'Error' + $StatusCode = [HttpStatusCode]::InternalServerError + } + + # 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/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyTemplate.ps1 new file mode 100644 index 000000000000..75ef6af27675 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyTemplate.ps1 @@ -0,0 +1,96 @@ +using namespace System.Net +Function Invoke-AddSafeLinksPolicyTemplate { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + Exchange.SafeLinks.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -Headers $Headers -API $APINAME -message 'Accessed this API' -Sev Debug + + # Debug: Log the incoming request body + Write-LogMessage -Headers $Headers -API $APINAME -message "Request body: $($Request.body | ConvertTo-Json -Depth 5 -Compress)" -Sev Debug + + try { + $GUID = (New-Guid).GUID + + # Validate required fields + if ([string]::IsNullOrEmpty($Request.body.Name)) { + throw "Template name is required but was not provided" + } + + if ([string]::IsNullOrEmpty($Request.body.PolicyName)) { + throw "Policy name is required but was not provided" + } + + # Create a new ordered hashtable to store selected properties + $policyObject = [ordered]@{} + + # Set name and comments - prioritize template-specific fields + $policyObject["TemplateName"] = $Request.body.Name + $policyObject["TemplateDescription"] = $Request.body.Description + + # For templates, if no specific policy description is provided, use template description as default + if ([string]::IsNullOrEmpty($Request.body.AdminDisplayName) -and -not [string]::IsNullOrEmpty($Request.body.Description)) { + $Request.body.AdminDisplayName = $Request.body.Description + Write-LogMessage -Headers $Headers -API $APINAME -message "Using template description as default policy description" -Sev Debug + } + + # Log what we're using for template name and description + Write-LogMessage -Headers $Headers -API $APINAME -message "Template Name: '$($policyObject.TemplateName)', Description: '$($policyObject.TemplateDescription)'" -Sev Debug + + # Copy specific properties we want to keep + $propertiesToKeep = @( + # Policy properties + "PolicyName", "EnableSafeLinksForEmail", "EnableSafeLinksForTeams", "EnableSafeLinksForOffice", + "TrackClicks", "AllowClickThrough", "ScanUrls", "EnableForInternalSenders", + "DeliverMessageAfterScan", "DisableUrlRewrite", "DoNotRewriteUrls", + "AdminDisplayName", "CustomNotificationText", "EnableOrganizationBranding", + # Rule properties + "RuleName", "Priority", "State", "Comments", + "SentTo", "SentToMemberOf", "RecipientDomainIs", + "ExceptIfSentTo", "ExceptIfSentToMemberOf", "ExceptIfRecipientDomainIs" + ) + + # Copy each property if it exists + foreach ($prop in $propertiesToKeep) { + if ($null -ne $Request.body.$prop) { + $policyObject[$prop] = $Request.body.$prop + Write-LogMessage -Headers $Headers -API $APINAME -message "Added property '$prop' with value '$($Request.body.$prop)'" -Sev Debug + } + } + + # Convert to JSON + $JSON = $policyObject | ConvertTo-Json -Depth 10 + Write-LogMessage -Headers $Headers -API $APINAME -message "Final JSON: $JSON" -Sev Debug + + # Save the template to Azure Table Storage + $Table = Get-CippTable -tablename 'templates' + $Table.Force = $true + Add-CIPPAzDataTableEntity @Table -Entity @{ + JSON = "$JSON" + RowKey = "$GUID" + PartitionKey = 'SafeLinksTemplate' + } + + Write-LogMessage -Headers $Headers -API $APINAME -message "Created SafeLinks Policy Template '$($policyObject.TemplateName)' with GUID $GUID" -Sev Info + $body = [pscustomobject]@{'Results' = "Created SafeLinks Policy Template '$($policyObject.TemplateName)' with GUID $GUID" } + $StatusCode = [HttpStatusCode]::OK + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -Headers $Headers -API $APINAME -message "Failed to create SafeLinks policy template: $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage + $body = [pscustomobject]@{'Results' = "Failed to create SafeLinks policy template: $($ErrorMessage.NormalizedError)" } + $StatusCode = [HttpStatusCode]::Forbidden + } + + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = $body + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-CreateSafeLinksPolicyTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-CreateSafeLinksPolicyTemplate.ps1 new file mode 100644 index 000000000000..9689856a1274 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-CreateSafeLinksPolicyTemplate.ps1 @@ -0,0 +1,77 @@ +using namespace System.Net + +Function Invoke-CreateSafeLinksPolicyTemplate { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + Exchange.SafeLinks.ReadWrite + .DESCRIPTION + This function creates a new Safe Links policy template from scratch. + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -Headers $Headers -API $APINAME -message 'Accessed this API' -Sev Debug + + try { + $GUID = (New-Guid).GUID + + # Create a new ordered hashtable to store selected properties + $policyObject = [ordered]@{} + + # Set name and comments first + $policyObject["TemplateName"] = $Request.body.Name + $policyObject["TemplateDescription"] = $Request.body.Description + + # Copy specific properties we want to keep + $propertiesToKeep = @( + # Policy properties + "PolicyName", "EnableSafeLinksForEmail", "EnableSafeLinksForTeams", "EnableSafeLinksForOffice", + "TrackClicks", "AllowClickThrough", "ScanUrls", "EnableForInternalSenders", + "DeliverMessageAfterScan", "DisableUrlRewrite", "DoNotRewriteUrls", + "AdminDisplayName", "CustomNotificationText", "EnableOrganizationBranding", + + # Rule properties + "RuleName", "Priority", "State", "Comments", + "SentTo", "SentToMemberOf", "RecipientDomainIs", + "ExceptIfSentTo", "ExceptIfSentToMemberOf", "ExceptIfRecipientDomainIs" + ) + + # Copy each property if it exists + foreach ($prop in $propertiesToKeep) { + if ($null -ne $Request.body.$prop) { + $policyObject[$prop] = $Request.body.$prop + } + } + + # Convert to JSON + $JSON = $policyObject | ConvertTo-Json -Depth 10 + + # Save the template to Azure Table Storage + $Table = Get-CippTable -tablename 'templates' + $Table.Force = $true + Add-CIPPAzDataTableEntity @Table -Entity @{ + JSON = "$JSON" + RowKey = "$GUID" + PartitionKey = 'SafeLinksTemplate' + } + + Write-LogMessage -Headers $Headers -API $APINAME -message "Created SafeLinks Policy Template $($policyObject.TemplateName) with GUID $GUID" -Sev Info + $body = [pscustomobject]@{'Results' = "Created SafeLinks Policy Template $($policyObject.TemplateName) with GUID $GUID" } + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -Headers $Headers -API $APINAME -message "Failed to create SafeLinks policy template: $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage + $body = [pscustomobject]@{'Results' = "Failed to create SafeLinks policy template: $($ErrorMessage.NormalizedError)" } + $StatusCode = [HttpStatusCode]::Forbidden + } + + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = $body + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-EditSafeLinksPolicy.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-EditSafeLinksPolicy.ps1 new file mode 100644 index 000000000000..2e70ec922fb6 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-EditSafeLinksPolicy.ps1 @@ -0,0 +1,217 @@ +using namespace System.Net + +function Invoke-EditSafeLinksPolicy { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Exchange.SpamFilter.ReadWrite + .DESCRIPTION + This function modifies an existing Safe Links policy and its associated rule. + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + + # Interact with query parameters or the body of the request. + $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter + $PolicyName = $Request.Query.PolicyName ?? $Request.Body.PolicyName + $RuleName = $Request.Query.RuleName ?? $Request.Body.RuleName + + # Helper function to process array fields + function Process-ArrayField { + param ( + [Parameter(Mandatory = $false)] + $Field + ) + + if ($null -eq $Field) { return @() } + + # If already an array, process each item + if ($Field -is [array]) { + $result = @() + foreach ($item in $Field) { + if ($item -is [string]) { + $result += $item + } + elseif ($item -is [hashtable] -or $item -is [PSCustomObject]) { + # Extract value from object + if ($null -ne $item.value) { + $result += $item.value + } + elseif ($null -ne $item.userPrincipalName) { + $result += $item.userPrincipalName + } + elseif ($null -ne $item.id) { + $result += $item.id + } + else { + $result += $item.ToString() + } + } + else { + $result += $item.ToString() + } + } + return $result + } + + # If it's a single object + if ($Field -is [hashtable] -or $Field -is [PSCustomObject]) { + if ($null -ne $Field.value) { return @($Field.value) } + if ($null -ne $Field.userPrincipalName) { return @($Field.userPrincipalName) } + if ($null -ne $Field.id) { return @($Field.id) } + } + + # If it's a string, return as an array with one item + if ($Field -is [string]) { + return @($Field) + } + + return @($Field) + } + + # Extract policy parameters from body + $EnableSafeLinksForEmail = $Request.Body.EnableSafeLinksForEmail + $EnableSafeLinksForTeams = $Request.Body.EnableSafeLinksForTeams + $EnableSafeLinksForOffice = $Request.Body.EnableSafeLinksForOffice + $TrackClicks = $Request.Body.TrackClicks + $AllowClickThrough = $Request.Body.AllowClickThrough + $ScanUrls = $Request.Body.ScanUrls + $EnableForInternalSenders = $Request.Body.EnableForInternalSenders + $DeliverMessageAfterScan = $Request.Body.DeliverMessageAfterScan + $DisableUrlRewrite = $Request.Body.DisableUrlRewrite + $DoNotRewriteUrls = Process-ArrayField -Field $Request.Body.DoNotRewriteUrls + $AdminDisplayName = $Request.Body.AdminDisplayName + $CustomNotificationText = $Request.Body.CustomNotificationText + $EnableOrganizationBranding = $Request.Body.EnableOrganizationBranding + + # Extract rule parameters from body + $Priority = $Request.Body.Priority + $Comments = $Request.Body.Comments + $Enabled = $Request.Body.State + + # Process recipient-related parameters + $SentTo = Process-ArrayField -Field $Request.Body.SentTo + $SentToMemberOf = Process-ArrayField -Field $Request.Body.SentToMemberOf + $RecipientDomainIs = Process-ArrayField -Field $Request.Body.RecipientDomainIs + $ExceptIfSentTo = Process-ArrayField -Field $Request.Body.ExceptIfSentTo + $ExceptIfSentToMemberOf = Process-ArrayField -Field $Request.Body.ExceptIfSentToMemberOf + $ExceptIfRecipientDomainIs = Process-ArrayField -Field $Request.Body.ExceptIfRecipientDomainIs + + $Results = [System.Collections.Generic.List[string]]@() + $hasPolicyParams = $false + $hasRuleParams = $false + $hasRuleOperation = $false + $ruleMessages = [System.Collections.Generic.List[string]]@() + + try { + # Check which types of updates we need to perform + # PART 1: Build and check policy parameters + $policyParams = @{ + Identity = $PolicyName + } + + # Only add parameters that are explicitly provided + if ($null -ne $EnableSafeLinksForEmail) { $policyParams.Add('EnableSafeLinksForEmail', $EnableSafeLinksForEmail); $hasPolicyParams = $true } + if ($null -ne $EnableSafeLinksForTeams) { $policyParams.Add('EnableSafeLinksForTeams', $EnableSafeLinksForTeams); $hasPolicyParams = $true } + if ($null -ne $EnableSafeLinksForOffice) { $policyParams.Add('EnableSafeLinksForOffice', $EnableSafeLinksForOffice); $hasPolicyParams = $true } + if ($null -ne $TrackClicks) { $policyParams.Add('TrackClicks', $TrackClicks); $hasPolicyParams = $true } + if ($null -ne $AllowClickThrough) { $policyParams.Add('AllowClickThrough', $AllowClickThrough); $hasPolicyParams = $true } + if ($null -ne $ScanUrls) { $policyParams.Add('ScanUrls', $ScanUrls); $hasPolicyParams = $true } + if ($null -ne $EnableForInternalSenders) { $policyParams.Add('EnableForInternalSenders', $EnableForInternalSenders); $hasPolicyParams = $true } + if ($null -ne $DeliverMessageAfterScan) { $policyParams.Add('DeliverMessageAfterScan', $DeliverMessageAfterScan); $hasPolicyParams = $true } + if ($null -ne $DisableUrlRewrite) { $policyParams.Add('DisableUrlRewrite', $DisableUrlRewrite); $hasPolicyParams = $true } + if ($null -ne $DoNotRewriteUrls -and $DoNotRewriteUrls.Count -gt 0) { $policyParams.Add('DoNotRewriteUrls', $DoNotRewriteUrls); $hasPolicyParams = $true } + if ($null -ne $AdminDisplayName) { $policyParams.Add('AdminDisplayName', $AdminDisplayName); $hasPolicyParams = $true } + if ($null -ne $CustomNotificationText) { $policyParams.Add('CustomNotificationText', $CustomNotificationText); $hasPolicyParams = $true } + if ($null -ne $EnableOrganizationBranding) { $policyParams.Add('EnableOrganizationBranding', $EnableOrganizationBranding); $hasPolicyParams = $true } + + # PART 2: Build and check rule parameters + $ruleParams = @{ + Identity = $RuleName + } + + # Add parameters that are explicitly provided + if ($null -ne $Comments) { $ruleParams.Add('Comments', $Comments); $hasRuleParams = $true } + if ($null -ne $Priority) { $ruleParams.Add('Priority', $Priority); $hasRuleParams = $true } + if ($SentTo.Count -gt 0) { $ruleParams.Add('SentTo', $SentTo); $hasRuleParams = $true } + if ($SentToMemberOf.Count -gt 0) { $ruleParams.Add('SentToMemberOf', $SentToMemberOf); $hasRuleParams = $true } + if ($RecipientDomainIs.Count -gt 0) { $ruleParams.Add('RecipientDomainIs', $RecipientDomainIs); $hasRuleParams = $true } + if ($ExceptIfSentTo.Count -gt 0) { $ruleParams.Add('ExceptIfSentTo', $ExceptIfSentTo); $hasRuleParams = $true } + if ($ExceptIfSentToMemberOf.Count -gt 0) { $ruleParams.Add('ExceptIfSentToMemberOf', $ExceptIfSentToMemberOf); $hasRuleParams = $true } + if ($ExceptIfRecipientDomainIs.Count -gt 0) { $ruleParams.Add('ExceptIfRecipientDomainIs', $ExceptIfRecipientDomainIs); $hasRuleParams = $true } + + # Now perform only the necessary operations + + # PART 1: Update policy if needed + if ($hasPolicyParams) { + $ExoPolicyRequestParam = @{ + tenantid = $TenantFilter + cmdlet = 'Set-SafeLinksPolicy' + cmdParams = $policyParams + useSystemMailbox = $true + } + + $null = New-ExoRequest @ExoPolicyRequestParam + $Results.Add("Successfully updated SafeLinks policy '$PolicyName'") + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Updated SafeLinks policy '$PolicyName'" -Sev 'Info' + } + + # PART 2: Update rule if needed + if ($hasRuleParams) { + $ExoRuleRequestParam = @{ + tenantid = $TenantFilter + cmdlet = 'Set-SafeLinksRule' + cmdParams = $ruleParams + useSystemMailbox = $true + } + + $null = New-ExoRequest @ExoRuleRequestParam + $hasRuleOperation = $true + $ruleMessages.Add("updated properties") + } + + # Handle enable/disable if needed + if ($null -ne $Enabled) { + $EnableCmdlet = $Enabled ? 'Enable-SafeLinksRule' : 'Disable-SafeLinksRule' + $EnableRequestParam = @{ + tenantid = $TenantFilter + cmdlet = $EnableCmdlet + cmdParams = @{ + Identity = $RuleName + } + useSystemMailbox = $true + } + + $null = New-ExoRequest @EnableRequestParam + $hasRuleOperation = $true + $state = $Enabled ? "enabled" : "disabled" + $ruleMessages.Add($state) + } + + # Add combined rule message if any rule operations were performed + if ($hasRuleOperation) { + $ruleOperations = $ruleMessages -join " and " + $Results.Add("Successfully $ruleOperations SafeLinks rule '$RuleName'") + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "$ruleOperations SafeLinks rule '$RuleName'" -Sev 'Info' + } + + $StatusCode = [HttpStatusCode]::OK + } + catch { + $ErrorMessage = Get-CippException -Exception $_ + $Results.Add("Failed updating SafeLinks configuration '$PolicyName'. Error: $($ErrorMessage.NormalizedError)") + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed updating SafeLinks configuration '$PolicyName'. Error: $($ErrorMessage.NormalizedError)" -Sev 'Error' + $StatusCode = [HttpStatusCode]::InternalServerError + } + + # 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/Security/Safe-Links-Policy/Invoke-EditSafeLinksPolicyTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-EditSafeLinksPolicyTemplate.ps1 new file mode 100644 index 000000000000..dc9bbf0fad5b --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-EditSafeLinksPolicyTemplate.ps1 @@ -0,0 +1,89 @@ +using namespace System.Net + +Function Invoke-EditSafeLinksPolicyTemplate { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + Exchange.SafeLinks.ReadWrite + .DESCRIPTION + This function updates an existing Safe Links policy template. + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -Headers $Headers -API $APINAME -message 'Accessed this API' -Sev Debug + + try { + $ID = $Request.Body.ID + + if (-not $ID) { + throw "Template ID is required" + } + + # Check if template exists + $Table = Get-CippTable -tablename 'templates' + $Filter = "PartitionKey eq 'SafeLinksTemplate' and RowKey eq '$ID'" + $ExistingTemplate = Get-CIPPAzDataTableEntity @Table -Filter $Filter + + if (-not $ExistingTemplate) { + throw "Template with ID '$ID' not found" + } + + # Create a new ordered hashtable to store selected properties + $policyObject = [ordered]@{} + + # Set name and comments + $policyObject["TemplateName"] = $Request.body.Name + $policyObject["TemplateDescription"] = $Request.body.Description + + # Copy specific properties we want to keep + $propertiesToKeep = @( + # Policy properties + "PolicyName", "EnableSafeLinksForEmail", "EnableSafeLinksForTeams", "EnableSafeLinksForOffice", + "TrackClicks", "AllowClickThrough", "ScanUrls", "EnableForInternalSenders", + "DeliverMessageAfterScan", "DisableUrlRewrite", "DoNotRewriteUrls", + "AdminDisplayName", "CustomNotificationText", "EnableOrganizationBranding", + + # Rule properties + "RuleName", "Priority", "State", "Comments", + "SentTo", "SentToMemberOf", "RecipientDomainIs", + "ExceptIfSentTo", "ExceptIfSentToMemberOf", "ExceptIfRecipientDomainIs" + ) + + # Copy each property if it exists + foreach ($prop in $propertiesToKeep) { + if ($null -ne $Request.body.$prop) { + $policyObject[$prop] = $Request.body.$prop + } + } + + # Convert to JSON + $JSON = $policyObject | ConvertTo-Json -Depth 10 + + # Update the template in Azure Table Storage + $Table.Force = $true + Add-CIPPAzDataTableEntity @Table -Entity @{ + JSON = "$JSON" + RowKey = "$ID" + PartitionKey = 'SafeLinksTemplate' + } + + Write-LogMessage -Headers $Headers -API $APINAME -message "Updated SafeLinks Policy Template $($policyObject.TemplateName) with ID $ID" -Sev Info + $body = [pscustomobject]@{'Results' = "Updated SafeLinks Policy Template $($policyObject.TemplateName) with ID $ID" } + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -Headers $Headers -API $APINAME -message "Failed to update SafeLinks policy template: $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage + $body = [pscustomobject]@{'Results' = "Failed to update SafeLinks policy template: $($ErrorMessage.NormalizedError)" } + $StatusCode = [HttpStatusCode]::Forbidden + } + + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = $body + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecDeleteSafeLinksPolicy.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecDeleteSafeLinksPolicy.ps1 new file mode 100644 index 000000000000..f14c2050a8e2 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecDeleteSafeLinksPolicy.ps1 @@ -0,0 +1,94 @@ +using namespace System.Net +function Invoke-ExecDeleteSafeLinksPolicy { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Exchange.SpamFilter.ReadWrite + .DESCRIPTION + This function deletes a Safe Links rule and its associated policy. + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + # Interact with query parameters or the body of the request. + $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter + $RuleName = $Request.Query.RuleName ?? $Request.Body.RuleName + $PolicyName = $Request.Query.PolicyName ?? $Request.Body.PolicyName + + $ResultMessages = @() + + try { + # Only try to delete the rule if a name was provided + if ($RuleName) { + try { + $ExoRequestRuleParam = @{ + tenantid = $TenantFilter + cmdlet = 'Remove-SafeLinksRule' + cmdParams = @{ + Identity = $RuleName + Confirm = $false + } + useSystemMailbox = $true + } + $null = New-ExoRequest @ExoRequestRuleParam + $ResultMessages += "Successfully deleted SafeLinks rule '$RuleName'" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Successfully deleted SafeLinks rule '$RuleName'" -Sev 'Info' + } + catch { + $ErrorMessage = Get-CippException -Exception $_ + $ResultMessages += "Failed to delete SafeLinks rule '$RuleName'. Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to delete SafeLinks rule '$RuleName'. Error: $($ErrorMessage.NormalizedError)" -Sev 'Warning' + } + } + else { + $ResultMessages += "No rule name provided, skipping rule deletion" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "No rule name provided, skipping rule deletion" -Sev 'Info' + } + + # Only try to delete the policy if a name was provided + if ($PolicyName) { + try { + $ExoRequestPolicyParam = @{ + tenantid = $TenantFilter + cmdlet = 'Remove-SafeLinksPolicy' + cmdParams = @{ + Identity = $PolicyName + Confirm = $false + } + useSystemMailbox = $true + } + $null = New-ExoRequest @ExoRequestPolicyParam + $ResultMessages += "Successfully deleted SafeLinks policy '$PolicyName'" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Successfully deleted SafeLinks policy '$PolicyName'" -Sev 'Info' + } + catch { + $ErrorMessage = Get-CippException -Exception $_ + $ResultMessages += "Failed to delete SafeLinks policy '$PolicyName'. Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to delete SafeLinks policy '$PolicyName'. Error: $($ErrorMessage.NormalizedError)" -Sev 'Warning' + } + } + else { + $ResultMessages += "No policy name provided, skipping policy deletion" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "No policy name provided, skipping policy deletion" -Sev 'Info' + } + + # Combine all result messages + $Result = $ResultMessages -join " | " + $StatusCode = [HttpStatusCode]::OK + } + catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "An unexpected error occurred: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev 'Error' + $StatusCode = [HttpStatusCode]::InternalServerError + } + + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{Results = $Result } + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecNewSafeLinksPolicy.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecNewSafeLinksPolicy.ps1 new file mode 100644 index 000000000000..2a29deecc7dc --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecNewSafeLinksPolicy.ps1 @@ -0,0 +1,237 @@ +using namespace System.Net + +function Invoke-ExecNewSafeLinksPolicy { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Exchange.SpamFilter.ReadWrite + .DESCRIPTION + This function creates a new Safe Links policy and an associated rule. + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + + # Interact with query parameters or the body of the request. + $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter + + # Extract policy settings from body + $PolicyName = $Request.Body.PolicyName + $EnableSafeLinksForEmail = $Request.Body.EnableSafeLinksForEmail + $EnableSafeLinksForTeams = $Request.Body.EnableSafeLinksForTeams + $EnableSafeLinksForOffice = $Request.Body.EnableSafeLinksForOffice + $TrackClicks = $Request.Body.TrackClicks + $AllowClickThrough = $Request.Body.AllowClickThrough + $ScanUrls = $Request.Body.ScanUrls + $EnableForInternalSenders = $Request.Body.EnableForInternalSenders + $DeliverMessageAfterScan = $Request.Body.DeliverMessageAfterScan + $DisableUrlRewrite = $Request.Body.DisableUrlRewrite + $DoNotRewriteUrls = $Request.Body.DoNotRewriteUrls + $AdminDisplayName = $Request.Body.AdminDisplayName + $CustomNotificationText = $Request.Body.CustomNotificationText + $EnableOrganizationBranding = $Request.Body.EnableOrganizationBranding + + # Extract rule settings from body + $Priority = $Request.Body.Priority + $Comments = $Request.Body.Comments + $Enabled = $Request.Body.State + $RuleName = $Request.Body.RuleName + + # Extract recipient fields and handle different input formats + $SentTo = $Request.Body.SentTo + $SentToMemberOf = $Request.Body.SentToMemberOf + $RecipientDomainIs = $Request.Body.RecipientDomainIs + $ExceptIfSentTo = $Request.Body.ExceptIfSentTo + $ExceptIfSentToMemberOf = $Request.Body.ExceptIfSentToMemberOf + $ExceptIfRecipientDomainIs = $Request.Body.ExceptIfRecipientDomainIs + + # Helper function to process array fields + function Process-ArrayField { + param ( + [Parameter(Mandatory = $false)] + $Field + ) + + if ($null -eq $Field) { return @() } + + # If already an array, process each item + if ($Field -is [array]) { + $result = @() + foreach ($item in $Field) { + if ($item -is [string]) { + $result += $item + } + elseif ($item -is [hashtable] -or $item -is [PSCustomObject]) { + # Extract value from object + if ($null -ne $item.value) { + $result += $item.value + } + elseif ($null -ne $item.userPrincipalName) { + $result += $item.userPrincipalName + } + elseif ($null -ne $item.id) { + $result += $item.id + } + else { + $result += $item.ToString() + } + } + else { + $result += $item.ToString() + } + } + return $result + } + + # If it's a single object + if ($Field -is [hashtable] -or $Field -is [PSCustomObject]) { + if ($null -ne $Field.value) { return @($Field.value) } + if ($null -ne $Field.userPrincipalName) { return @($Field.userPrincipalName) } + if ($null -ne $Field.id) { return @($Field.id) } + } + + # If it's a string, return as an array with one item + if ($Field -is [string]) { + return @($Field) + } + + return @($Field) + } + + # Process all array fields + $SentTo = Process-ArrayField -Field $SentTo + $SentToMemberOf = Process-ArrayField -Field $SentToMemberOf + $RecipientDomainIs = Process-ArrayField -Field $RecipientDomainIs + $ExceptIfSentTo = Process-ArrayField -Field $ExceptIfSentTo + $ExceptIfSentToMemberOf = Process-ArrayField -Field $ExceptIfSentToMemberOf + $ExceptIfRecipientDomainIs = Process-ArrayField -Field $ExceptIfRecipientDomainIs + $DoNotRewriteUrls = Process-ArrayField -Field $DoNotRewriteUrls + + try { + # Check if policy exists by listing all policies and filtering + $ExistingPoliciesParam = @{ + tenantid = $TenantFilter + cmdlet = 'Get-SafeLinksPolicy' + useSystemMailbox = $true + } + + $ExistingPolicies = New-ExoRequest @ExistingPoliciesParam + $PolicyExists = $ExistingPolicies | Where-Object { $_.Name -eq $PolicyName } + + if ($PolicyExists) { + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Policy with name '$PolicyName' already exists in tenant $TenantFilter" -Sev 'Warning' + "Policy with name '$PolicyName' already exists in tenant $TenantFilter" + continue + } + + # Check if rule exists by listing all rules and filtering + $ExistingRulesParam = @{ + tenantid = $TenantFilter + cmdlet = 'Get-SafeLinksRule' + useSystemMailbox = $true + } + + $ExistingRules = New-ExoRequest @ExistingRulesParam + $RuleExists = $ExistingRules | Where-Object { $_.Name -eq $RuleName } + + if ($RuleExists) { + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Rule with name '$RuleName' already exists in tenant $TenantFilter" -Sev 'Warning' + "Rule with name '$RuleName' already exists in tenant $TenantFilter" + continue + } + # PART 1: Create SafeLinks Policy + # Build command parameters for policy + $policyParams = @{ + Name = $PolicyName + } + + # Only add parameters that are explicitly provided + if ($null -ne $EnableSafeLinksForEmail) { $policyParams.Add('EnableSafeLinksForEmail', $EnableSafeLinksForEmail) } + if ($null -ne $EnableSafeLinksForTeams) { $policyParams.Add('EnableSafeLinksForTeams', $EnableSafeLinksForTeams) } + if ($null -ne $EnableSafeLinksForOffice) { $policyParams.Add('EnableSafeLinksForOffice', $EnableSafeLinksForOffice) } + if ($null -ne $TrackClicks) { $policyParams.Add('TrackClicks', $TrackClicks) } + if ($null -ne $AllowClickThrough) { $policyParams.Add('AllowClickThrough', $AllowClickThrough) } + if ($null -ne $ScanUrls) { $policyParams.Add('ScanUrls', $ScanUrls) } + if ($null -ne $EnableForInternalSenders) { $policyParams.Add('EnableForInternalSenders', $EnableForInternalSenders) } + if ($null -ne $DeliverMessageAfterScan) { $policyParams.Add('DeliverMessageAfterScan', $DeliverMessageAfterScan) } + if ($null -ne $DisableUrlRewrite) { $policyParams.Add('DisableUrlRewrite', $DisableUrlRewrite) } + if ($null -ne $DoNotRewriteUrls -and $DoNotRewriteUrls.Count -gt 0) { $policyParams.Add('DoNotRewriteUrls', $DoNotRewriteUrls) } + if ($null -ne $AdminDisplayName) { $policyParams.Add('AdminDisplayName', $AdminDisplayName) } + if ($null -ne $CustomNotificationText) { $policyParams.Add('CustomNotificationText', $CustomNotificationText) } + if ($null -ne $EnableOrganizationBranding) { $policyParams.Add('EnableOrganizationBranding', $EnableOrganizationBranding) } + + $ExoPolicyRequestParam = @{ + tenantid = $TenantFilter + cmdlet = 'New-SafeLinksPolicy' + cmdParams = $policyParams + useSystemMailbox = $true + } + + $null = New-ExoRequest @ExoPolicyRequestParam + $PolicyResult = "Successfully created new SafeLinks policy '$PolicyName'" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $PolicyResult -Sev 'Info' + + # PART 2: Create SafeLinks Rule + # Build command parameters for rule + $ruleParams = @{ + Name = $RuleName + SafeLinksPolicy = $PolicyName + } + + # Only add parameters that are explicitly provided + if ($null -ne $Priority) { $ruleParams.Add('Priority', $Priority) } + if ($null -ne $Comments) { $ruleParams.Add('Comments', $Comments) } + if ($null -ne $SentTo -and $SentTo.Count -gt 0) { $ruleParams.Add('SentTo', $SentTo) } + if ($null -ne $SentToMemberOf -and $SentToMemberOf.Count -gt 0) { $ruleParams.Add('SentToMemberOf', $SentToMemberOf) } + if ($null -ne $RecipientDomainIs -and $RecipientDomainIs.Count -gt 0) { $ruleParams.Add('RecipientDomainIs', $RecipientDomainIs) } + if ($null -ne $ExceptIfSentTo -and $ExceptIfSentTo.Count -gt 0) { $ruleParams.Add('ExceptIfSentTo', $ExceptIfSentTo) } + if ($null -ne $ExceptIfSentToMemberOf -and $ExceptIfSentToMemberOf.Count -gt 0) { $ruleParams.Add('ExceptIfSentToMemberOf', $ExceptIfSentToMemberOf) } + if ($null -ne $ExceptIfRecipientDomainIs -and $ExceptIfRecipientDomainIs.Count -gt 0) { $ruleParams.Add('ExceptIfRecipientDomainIs', $ExceptIfRecipientDomainIs) } + + $ExoRuleRequestParam = @{ + tenantid = $TenantFilter + cmdlet = 'New-SafeLinksRule' + cmdParams = $ruleParams + useSystemMailbox = $true + } + + $null = New-ExoRequest @ExoRuleRequestParam + + # If Enabled is specified, enable or disable the rule + if ($null -ne $Enabled) { + $EnableCmdlet = $Enabled ? 'Enable-SafeLinksRule' : 'Disable-SafeLinksRule' + $EnableRequestParam = @{ + tenantid = $TenantFilter + cmdlet = $EnableCmdlet + cmdParams = @{ + Identity = $RuleName + } + useSystemMailbox = $true + } + + $null = New-ExoRequest @EnableRequestParam + } + + $RuleResult = "Successfully created new SafeLinks rule '$RuleName'" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $RuleResult -Sev 'Info' + + $Result = "Successfully created new SafeLinks policy '$PolicyName'and rule '$RuleName'" + $StatusCode = [HttpStatusCode]::OK + } + catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed creating new SafeLinks policy '$PolicyName'and rule '$RuleName'. Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev 'Error' + $StatusCode = [HttpStatusCode]::InternalServerError + } + + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{Results = $Result } + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicy.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicy.ps1 new file mode 100644 index 000000000000..23a1a9c2a6b9 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicy.ps1 @@ -0,0 +1,94 @@ +using namespace System.Net +Function Invoke-ListSafeLinksPolicy { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.SafeLinksPolicy.Read + .DESCRIPTION + This function is used to list the Safe Links policies in the tenant. + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + $Tenantfilter = $request.Query.tenantfilter + + try { + $Policies = New-ExoRequest -tenantid $Tenantfilter -cmdlet 'Get-SafeLinksPolicy' | Select-Object -Property * -ExcludeProperty '*@odata.type' , '*@data.type' + $Rules = New-ExoRequest -tenantid $Tenantfilter -cmdlet 'Get-SafeLinksRule' | Select-Object -Property * -ExcludeProperty '*@odata.type' , '*@data.type' + $BuiltInRules = New-ExoRequest -tenantid $Tenantfilter -cmdlet 'Get-EOPProtectionPolicyRule' | Select-Object -Property * -ExcludeProperty '*@odata.type' , '*@data.type' + + # Single-pass processing optimized for Azure Functions + $Output = foreach ($policy in $Policies) { + $policyName = $policy.Name + + # Find associated rule (single lookup per policy) + $associatedRule = $null + foreach ($rule in $Rules) { + if ($rule.SafeLinksPolicy -eq $policyName) { + $associatedRule = $rule + break + } + } + + # Find matching built-in rule (single lookup per policy) + $matchingBuiltInRule = $null + foreach ($builtInRule in $BuiltInRules) { + if ($policyName -like "$($builtInRule.Name)*") { + $matchingBuiltInRule = $builtInRule + break + } + } + + # Create output object with all properties in one go + [PSCustomObject]@{ + # Copy all original policy properties + Name = $policy.Name + AdminDisplayName = $policy.AdminDisplayName + EnableSafeLinksForEmail = $policy.EnableSafeLinksForEmail + EnableSafeLinksForTeams = $policy.EnableSafeLinksForTeams + EnableSafeLinksForOffice = $policy.EnableSafeLinksForOffice + TrackClicks = $policy.TrackClicks + AllowClickThrough = $policy.AllowClickThrough + ScanUrls = $policy.ScanUrls + EnableForInternalSenders = $policy.EnableForInternalSenders + DeliverMessageAfterScan = $policy.DeliverMessageAfterScan + DisableUrlRewrite = $policy.DisableUrlRewrite + DoNotRewriteUrls = $policy.DoNotRewriteUrls + CustomNotificationText = $policy.CustomNotificationText + EnableOrganizationBranding = $policy.EnableOrganizationBranding + + # Calculated properties + PolicyName = $policyName + RuleName = $associatedRule.Name + Priority = if ($matchingBuiltInRule) { $matchingBuiltInRule.Priority } else { $associatedRule.Priority } + State = if ($matchingBuiltInRule) { $matchingBuiltInRule.State } else { $associatedRule.State } + SentTo = $associatedRule.SentTo + SentToMemberOf = $associatedRule.SentToMemberOf + RecipientDomainIs = $associatedRule.RecipientDomainIs + ExceptIfSentTo = $associatedRule.ExceptIfSentTo + ExceptIfSentToMemberOf = $associatedRule.ExceptIfSentToMemberOf + ExceptIfRecipientDomainIs = $associatedRule.ExceptIfRecipientDomainIs + Description = $policy.AdminDisplayName + IsBuiltIn = ($matchingBuiltInRule -ne $null) + } + } + + Write-LogMessage -headers $Headers -API $APIName -message "Retrieved $($Output.Count) Safe Links policies" -Sev 'Info' + $StatusCode = [HttpStatusCode]::OK + } + catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -headers $Headers -API $APIName -message "Error retrieving Safe Links policies: $ErrorMessage" -Sev 'Error' + $StatusCode = [HttpStatusCode]::Forbidden + $Output = $ErrorMessage + } + + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = $Output + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicyDetails.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicyDetails.ps1 new file mode 100644 index 000000000000..ecb731c415a9 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicyDetails.ps1 @@ -0,0 +1,106 @@ +using namespace System.Net +function Invoke-ListSafeLinksPolicyDetails { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Exchange.SpamFilter.Read + .DESCRIPTION + This function retrieves details for a specific Safe Links policy and rule. + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + + # Interact with query parameters or the body of the request. + $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter + $PolicyName = $Request.Query.PolicyName ?? $Request.Body.PolicyName + $RuleName = $Request.Query.RuleName ?? $Request.Body.RuleName + + $Result = @{} + $LogMessages = @() + + try { + # Get policy details if PolicyName is provided + if ($PolicyName) { + try { + $PolicyRequestParam = @{ + tenantid = $TenantFilter + cmdlet = 'Get-SafeLinksPolicy' + cmdParams = @{ + Identity = $PolicyName + } + useSystemMailbox = $true + } + $PolicyDetails = New-ExoRequest @PolicyRequestParam + $Result.Policy = $PolicyDetails + $Result.PolicyName = $PolicyDetails.Name + $LogMessages += "Successfully retrieved details for SafeLinks policy '$PolicyName'" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Successfully retrieved details for SafeLinks policy '$PolicyName'" -Sev 'Info' + } + catch { + $ErrorMessage = Get-CippException -Exception $_ + $LogMessages += "Failed to retrieve details for SafeLinks policy '$PolicyName'. Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to retrieve details for SafeLinks policy '$PolicyName'. Error: $($ErrorMessage.NormalizedError)" -Sev 'Warning' + $Result.PolicyError = "Failed to retrieve: $($ErrorMessage.NormalizedError)" + } + } + else { + $LogMessages += "No policy name provided, skipping policy retrieval" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "No policy name provided, skipping policy retrieval" -Sev 'Info' + } + + # Get rule details if RuleName is provided + if ($RuleName) { + try { + $RuleRequestParam = @{ + tenantid = $TenantFilter + cmdlet = 'Get-SafeLinksRule' + cmdParams = @{ + Identity = $RuleName + } + useSystemMailbox = $true + } + $RuleDetails = New-ExoRequest @RuleRequestParam + $Result.Rule = $RuleDetails + $Result.RuleName = $RuleDetails.Name + $LogMessages += "Successfully retrieved details for SafeLinks rule '$RuleName'" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Successfully retrieved details for SafeLinks rule '$RuleName'" -Sev 'Info' + } + catch { + $ErrorMessage = Get-CippException -Exception $_ + $LogMessages += "Failed to retrieve details for SafeLinks rule '$RuleName'. Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to retrieve details for SafeLinks rule '$RuleName'. Error: $($ErrorMessage.NormalizedError)" -Sev 'Warning' + $Result.RuleError = "Failed to retrieve: $($ErrorMessage.NormalizedError)" + } + } + else { + $LogMessages += "No rule name provided, skipping rule retrieval" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "No rule name provided, skipping rule retrieval" -Sev 'Info' + } + + # If no valid retrievals were performed, throw an error + if (-not ($Result.Policy -or $Result.Rule)) { + throw "No valid policy or rule details could be retrieved" + } + + # Set success status + $StatusCode = [HttpStatusCode]::OK + $Result.Message = $LogMessages -join " | " + } + catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Operation failed: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev 'Error' + $StatusCode = [HttpStatusCode]::InternalServerError + } + + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{Results = $Result } + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicyTemplateDetails.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicyTemplateDetails.ps1 new file mode 100644 index 000000000000..26f19bceb065 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicyTemplateDetails.ps1 @@ -0,0 +1,57 @@ +using namespace System.Net +Function Invoke-ListSafeLinksPolicyTemplateDetails { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + Exchange.SafeLinks.Read + .DESCRIPTION + This function retrieves details for a specific Safe Links policy template. + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + + # Get the template ID from query parameters + $ID = $Request.Query.ID ?? $Request.Body.ID + + $Result = @{} + + try { + if (-not $ID) { + throw "Template ID is required" + } + + # Get the specific template from Azure Table Storage + $Table = Get-CippTable -tablename 'templates' + $Filter = "PartitionKey eq 'SafeLinksTemplate' and RowKey eq '$ID'" + $Template = Get-CIPPAzDataTableEntity @Table -Filter $Filter + + if (-not $Template) { + throw "Template with ID '$ID' not found" + } + + # Parse the JSON data and add metadata + $TemplateData = $Template.JSON | ConvertFrom-Json + $TemplateData | Add-Member -NotePropertyName 'GUID' -NotePropertyValue $Template.RowKey -Force + + $Result = $TemplateData + $StatusCode = [HttpStatusCode]::OK + Write-LogMessage -headers $Headers -API $APIName -message "Successfully retrieved template details for ID '$ID'" -Sev 'Info' + } + catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to retrieve template details for ID '$ID'. Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Error' + $StatusCode = [HttpStatusCode]::InternalServerError + } + + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{Results = $Result } + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicyTemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicyTemplates.ps1 new file mode 100644 index 000000000000..5c4477985199 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicyTemplates.ps1 @@ -0,0 +1,39 @@ +using namespace System.Net +Function Invoke-ListSafeLinksPolicyTemplates { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + Exchange.SafeLinks.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + $Table = Get-CippTable -tablename 'templates' + $Templates = Get-ChildItem 'Config\*.SafeLinksTemplate.json' | ForEach-Object { + $Entity = @{ + JSON = "$(Get-Content $_)" + RowKey = "$($_.name)" + PartitionKey = 'SafeLinksTemplate' + GUID = "$($_.name)" + } + Add-CIPPAzDataTableEntity @Table -Entity $Entity -Force + } + #List policies + $Table = Get-CippTable -tablename 'templates' + $Filter = "PartitionKey eq 'SafeLinksTemplate'" + $Templates = (Get-CIPPAzDataTableEntity @Table -Filter $Filter) | ForEach-Object { + $GUID = $_.RowKey + $data = $_.JSON | ConvertFrom-Json + $data | Add-Member -NotePropertyName 'GUID' -NotePropertyValue $GUID + $data + } + if ($Request.query.ID) { $Templates = $Templates | Where-Object -Property RowKey -EQ $Request.query.id } + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = @($Templates) + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-RemoveSafeLinksPolicyTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-RemoveSafeLinksPolicyTemplate.ps1 new file mode 100644 index 000000000000..676b72e4b17e --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-RemoveSafeLinksPolicyTemplate.ps1 @@ -0,0 +1,35 @@ +using namespace System.Net + +Function Invoke-RemoveSafeLinksPolicyTemplate { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + Exchange.SafeLinks.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + $APIName = $Request.Params.CIPPEndpoint + $User = $Request.Headers + Write-LogMessage -Headers $User -API $APINAME -message 'Accessed this API' -Sev 'Debug' + $ID = $request.query.ID ?? $request.body.ID + try { + $Table = Get-CippTable -tablename 'templates' + $Filter = "PartitionKey eq 'SafeLinksTemplate' and RowKey eq '$id'" + $ClearRow = Get-CIPPAzDataTableEntity @Table -Filter $Filter -Property PartitionKey, RowKey + Remove-AzDataTableEntity -Force @Table -Entity $ClearRow + $Result = "Removed SafeLinks Policy Template with ID $ID." + Write-LogMessage -Headers $User -API $APINAME -message $Result -Sev 'Info' + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to remove SafeLinks Policy template with ID $ID. Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -Headers $User -API $APINAME -message $Result -Sev 'Error' -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::Forbidden + } + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{ Results = $Result } + }) +} diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksTemplatePolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksTemplatePolicy.ps1 new file mode 100644 index 000000000000..84c654359d2e --- /dev/null +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksTemplatePolicy.ps1 @@ -0,0 +1,538 @@ +function Invoke-CIPPStandardSafeLinksTemplatePolicy { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) SafeLinksTemplatePolicy + .SYNOPSIS + (Label) SafeLinks Policy Template + .DESCRIPTION + (Helptext) This applies selected SafeLinks policy templates to the tenant, creating or updating as needed + (DocsDescription) This applies selected SafeLinks policy templates to the tenant, creating or updating as needed + .NOTES + CAT + Defender Standards + TAG + "CIS" + "mdo_safelinksforemail" + "mdo_safelinksforOfficeApps" + ADDEDCOMPONENT + {"type":"autoComplete","multiple":true,"name":"standards.SafeLinksTemplatePolicy.TemplateIds","label":"SafeLinks Templates","loadingMessage":"Loading templates...","api":{"url":"/api/ListSafeLinksPolicyTemplates","labelField":"name","valueField":"GUID","queryKey":"ListSafeLinksPolicyTemplates"}} + IMPACT + Low Impact + ADDEDDATE + 2025-04-29 + POWERSHELLEQUIVALENT + New-SafeLinksPolicy, Set-SafeLinksPolicy, New-SafeLinksRule, Set-SafeLinksRule + RECOMMENDEDBY + "CIS" + .LINK + https://docs.cipp.app/user-documentation/tenant/standards/list-standards/defender-standards#low-impact + #> + + param($Tenant, $Settings) + ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'SafeLinksPolicy' + + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Processing SafeLinks template with settings: $($Settings | ConvertTo-Json -Compress)" -sev Debug + + # Verify tenant has necessary license + $ServicePlans = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/subscribedSkus?$select=servicePlans' -tenantid $Tenant + $ServicePlans = $ServicePlans.servicePlans.servicePlanName + $MDOLicensed = $ServicePlans -contains 'ATP_ENTERPRISE' + + if (-not $MDOLicensed) { + if ($Settings.remediate -eq $true) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Failed to apply SafeLinks templates: Tenant does not have Microsoft Defender for Office 365 license' -sev Error + } + + if ($Settings.alert -eq $true) { + Write-StandardsAlert -message 'SafeLinks templates could not be applied: Tenant does not have Microsoft Defender for Office 365 license' -object $MDOLicensed -tenant $Tenant -standardName 'SafeLinksTemplatePolicy' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'SafeLinks templates could not be applied: Tenant does not have Microsoft Defender for Office 365 license' -sev Info + } + + if ($Settings.report -eq $true) { + Add-CIPPBPAField -FieldName 'SafeLinksTemplatePolicy' -FieldValue $false -StoreAs bool -Tenant $tenant + Set-CIPPStandardsCompareField -FieldName 'standards.SafeLinksTemplatePolicy' -FieldValue $false -Tenant $Tenant + } + + return + } + + # Handle remediation + If ($Settings.remediate -eq $true) { + # Normalize the template list property based on what's passed - support multiple possible formats + if ($Settings.'standards.SafeLinksTemplatePolicy.TemplateIds') { + $Settings | Add-Member -NotePropertyName 'TemplateList' -NotePropertyValue $Settings.'standards.SafeLinksTemplatePolicy.TemplateIds' -Force + } elseif ($Settings.TemplateIds) { + $Settings | Add-Member -NotePropertyName 'TemplateList' -NotePropertyValue $Settings.TemplateIds -Force + } + + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Template list after normalization: $($Settings.TemplateList | ConvertTo-Json -Compress)" -sev Debug + + if (-not $Settings.TemplateList) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to apply SafeLinks templates: No templates selected" -sev Error + return + } + + # Initialize overall results tracking + $OverallSuccess = $true + $TemplateResults = @{} + + # Process each template + foreach ($Template in $Settings.TemplateList) { + $TemplateId = $Template.value + Write-Host "Working on template ID: $TemplateId" + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Processing SafeLinks template with ID: $TemplateId" -sev Info + + # Get the template by GUID + $Table = Get-CippTable -tablename 'templates' + $Filter = "PartitionKey eq 'SafeLinksTemplate' and RowKey eq '$TemplateId'" + $Template = Get-CIPPAzDataTableEntity @Table -Filter $Filter + + if (-not $Template) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to apply SafeLinks template: Template with ID $TemplateId not found" -sev Error + $TemplateResults[$TemplateId] = @{ + Success = $false + Message = "Template with ID $TemplateId not found" + } + $OverallSuccess = $false + continue + } + + # Parse the template JSON + try { + $PolicyConfig = $Template.JSON | ConvertFrom-Json -ErrorAction Stop + + # Helper function to process array fields + function Process-ArrayField { + param ( + [Parameter(Mandatory = $false)] + $Field + ) + + if ($null -eq $Field) { return @() } + + # If already an array, process each item + if ($Field -is [array]) { + $result = @() + foreach ($item in $Field) { + if ($item -is [string]) { + $result += $item + } + elseif ($item -is [hashtable] -or $item -is [PSCustomObject]) { + # Extract value from object + if ($null -ne $item.value) { + $result += $item.value + } + elseif ($null -ne $item.userPrincipalName) { + $result += $item.userPrincipalName + } + elseif ($null -ne $item.id) { + $result += $item.id + } + else { + $result += $item.ToString() + } + } + else { + $result += $item.ToString() + } + } + return $result + } + + # If it's a single object + if ($Field -is [hashtable] -or $Field -is [PSCustomObject]) { + if ($null -ne $Field.value) { return @($Field.value) } + if ($null -ne $Field.userPrincipalName) { return @($Field.userPrincipalName) } + if ($null -ne $Field.id) { return @($Field.id) } + } + + # If it's a string, return as an array with one item + if ($Field -is [string]) { + return @($Field) + } + + return @($Field) + } + + # Extract policy name from template + $PolicyName = $PolicyConfig.PolicyName ?? $PolicyConfig.Name + $RuleName = $PolicyConfig.RuleName ?? $PolicyName + + # Process arrays in the template + $DoNotRewriteUrls = Process-ArrayField -Field $PolicyConfig.DoNotRewriteUrls + $SentTo = Process-ArrayField -Field $PolicyConfig.SentTo + $SentToMemberOf = Process-ArrayField -Field $PolicyConfig.SentToMemberOf + $RecipientDomainIs = Process-ArrayField -Field $PolicyConfig.RecipientDomainIs + $ExceptIfSentTo = Process-ArrayField -Field $PolicyConfig.ExceptIfSentTo + $ExceptIfSentToMemberOf = Process-ArrayField -Field $PolicyConfig.ExceptIfSentToMemberOf + $ExceptIfRecipientDomainIs = Process-ArrayField -Field $PolicyConfig.ExceptIfRecipientDomainIs + + # Check if policy and rule exist + $ExistingPoliciesParam = @{ + tenantid = $Tenant + cmdlet = 'Get-SafeLinksPolicy' + useSystemMailbox = $true + } + + $ExistingPolicies = New-ExoRequest @ExistingPoliciesParam + $PolicyExists = $ExistingPolicies | Where-Object { $_.Name -eq $PolicyName } + + $ExistingRulesParam = @{ + tenantid = $Tenant + cmdlet = 'Get-SafeLinksRule' + useSystemMailbox = $true + } + + $ExistingRules = New-ExoRequest @ExistingRulesParam + $RuleExists = $ExistingRules | Where-Object { $_.Name -eq $RuleName } + + # Build policy parameters + $policyParams = @{} + + # Only add parameters that are explicitly provided in the template + if ($null -ne $PolicyConfig.EnableSafeLinksForEmail) { $policyParams.Add('EnableSafeLinksForEmail', $PolicyConfig.EnableSafeLinksForEmail) } + if ($null -ne $PolicyConfig.EnableSafeLinksForTeams) { $policyParams.Add('EnableSafeLinksForTeams', $PolicyConfig.EnableSafeLinksForTeams) } + if ($null -ne $PolicyConfig.EnableSafeLinksForOffice) { $policyParams.Add('EnableSafeLinksForOffice', $PolicyConfig.EnableSafeLinksForOffice) } + if ($null -ne $PolicyConfig.TrackClicks) { $policyParams.Add('TrackClicks', $PolicyConfig.TrackClicks) } + if ($null -ne $PolicyConfig.AllowClickThrough) { $policyParams.Add('AllowClickThrough', $PolicyConfig.AllowClickThrough) } + if ($null -ne $PolicyConfig.ScanUrls) { $policyParams.Add('ScanUrls', $PolicyConfig.ScanUrls) } + if ($null -ne $PolicyConfig.EnableForInternalSenders) { $policyParams.Add('EnableForInternalSenders', $PolicyConfig.EnableForInternalSenders) } + if ($null -ne $PolicyConfig.DeliverMessageAfterScan) { $policyParams.Add('DeliverMessageAfterScan', $PolicyConfig.DeliverMessageAfterScan) } + if ($null -ne $PolicyConfig.DisableUrlRewrite) { $policyParams.Add('DisableUrlRewrite', $PolicyConfig.DisableUrlRewrite) } + if ($null -ne $DoNotRewriteUrls -and $DoNotRewriteUrls.Count -gt 0) { $policyParams.Add('DoNotRewriteUrls', $DoNotRewriteUrls) } + if ($null -ne $PolicyConfig.AdminDisplayName) { $policyParams.Add('AdminDisplayName', $PolicyConfig.AdminDisplayName) } + if ($null -ne $PolicyConfig.CustomNotificationText) { $policyParams.Add('CustomNotificationText', $PolicyConfig.CustomNotificationText) } + if ($null -ne $PolicyConfig.EnableOrganizationBranding) { $policyParams.Add('EnableOrganizationBranding', $PolicyConfig.EnableOrganizationBranding) } + + # Build rule parameters + $ruleParams = @{} + + # Only add parameters that are explicitly provided + if ($null -ne $PolicyConfig.Priority) { $ruleParams.Add('Priority', $PolicyConfig.Priority) } + if ($null -ne $PolicyConfig.Description) { $ruleParams.Add('Comments', $PolicyConfig.Description) } + if ($null -ne $SentTo -and $SentTo.Count -gt 0) { $ruleParams.Add('SentTo', $SentTo) } + if ($null -ne $SentToMemberOf -and $SentToMemberOf.Count -gt 0) { $ruleParams.Add('SentToMemberOf', $SentToMemberOf) } + if ($null -ne $RecipientDomainIs -and $RecipientDomainIs.Count -gt 0) { $ruleParams.Add('RecipientDomainIs', $RecipientDomainIs) } + if ($null -ne $ExceptIfSentTo -and $ExceptIfSentTo.Count -gt 0) { $ruleParams.Add('ExceptIfSentTo', $ExceptIfSentTo) } + if ($null -ne $ExceptIfSentToMemberOf -and $ExceptIfSentToMemberOf.Count -gt 0) { $ruleParams.Add('ExceptIfSentToMemberOf', $ExceptIfSentToMemberOf) } + if ($null -ne $ExceptIfRecipientDomainIs -and $ExceptIfRecipientDomainIs.Count -gt 0) { $ruleParams.Add('ExceptIfRecipientDomainIs', $ExceptIfRecipientDomainIs) } + + $ActionsTaken = @() + + try { + if ($PolicyExists) { + # Update existing policy + $policyParams.Add('Identity', $PolicyName) + + $ExoPolicyRequestParam = @{ + tenantid = $Tenant + cmdlet = 'Set-SafeLinksPolicy' + cmdParams = $policyParams + useSystemMailbox = $true + } + + $null = New-ExoRequest @ExoPolicyRequestParam + $ActionsTaken += "Updated SafeLinks policy '$PolicyName'" + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Updated SafeLinks policy '$PolicyName'" -sev 'Info' + } + else { + # Create new policy + $policyParams.Add('Name', $PolicyName) + + $ExoPolicyRequestParam = @{ + tenantid = $Tenant + cmdlet = 'New-SafeLinksPolicy' + cmdParams = $policyParams + useSystemMailbox = $true + } + + $null = New-ExoRequest @ExoPolicyRequestParam + $ActionsTaken += "Created new SafeLinks policy '$PolicyName'" + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Created new SafeLinks policy '$PolicyName'" -sev 'Info' + } + + if ($RuleExists) { + # Update existing rule + $ruleParams.Add('Identity', $RuleName) + + $ExoRuleRequestParam = @{ + tenantid = $Tenant + cmdlet = 'Set-SafeLinksRule' + cmdParams = $ruleParams + useSystemMailbox = $true + } + + $null = New-ExoRequest @ExoRuleRequestParam + $ActionsTaken += "Updated SafeLinks rule '$RuleName'" + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Updated SafeLinks rule '$RuleName'" -sev 'Info' + } + else { + # Create new rule + $ruleParams.Add('Name', $RuleName) + $ruleParams.Add('SafeLinksPolicy', $PolicyName) + + $ExoRuleRequestParam = @{ + tenantid = $Tenant + cmdlet = 'New-SafeLinksRule' + cmdParams = $ruleParams + useSystemMailbox = $true + } + + $null = New-ExoRequest @ExoRuleRequestParam + $ActionsTaken += "Created new SafeLinks rule '$RuleName'" + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Created new SafeLinks rule '$RuleName'" -sev 'Info' + } + + # If State is specified in the template, enable or disable the rule + if ($null -ne $PolicyConfig.State) { + $Enabled = $PolicyConfig.State -eq "Enabled" + $EnableCmdlet = $Enabled ? 'Enable-SafeLinksRule' : 'Disable-SafeLinksRule' + $EnableRequestParam = @{ + tenantid = $Tenant + cmdlet = $EnableCmdlet + cmdParams = @{ + Identity = $RuleName + } + useSystemMailbox = $true + } + + $null = New-ExoRequest @EnableRequestParam + $StateMsg = $Enabled ? "enabled" : "disabled" + $ActionsTaken += "Rule $StateMsg" + Write-LogMessage -API 'Standards' -tenant $Tenant -message "SafeLinks rule '$RuleName' $StateMsg" -sev 'Info' + } + + $TemplateResults[$TemplateId] = @{ + Success = $true + ActionsTaken = $ActionsTaken + TemplateName = $PolicyConfig.Name + PolicyName = $PolicyName + RuleName = $RuleName + } + + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully applied SafeLinks template '$($PolicyConfig.Name)'" -sev 'Info' + } + catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + $TemplateResults[$TemplateId] = @{ + Success = $false + Message = $ErrorMessage + TemplateName = $PolicyConfig.Name + } + $OverallSuccess = $false + + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to apply SafeLinks template '$($PolicyConfig.Name)': $ErrorMessage" -sev 'Error' + } + } + catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + $TemplateResults[$TemplateId] = @{ + Success = $false + Message = $ErrorMessage + } + $OverallSuccess = $false + + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to process template with ID $($TemplateId): $ErrorMessage" -sev 'Error' + } + } + + # Report on overall results + if ($OverallSuccess) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully applied all SafeLinks templates" -sev 'Info' + } else { + $SuccessCount = ($TemplateResults.Values | Where-Object { $_.Success -eq $true }).Count + $TotalCount = $Settings.TemplateList.Count + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Applied $SuccessCount out of $TotalCount SafeLinks templates" -sev 'Info' + } + } + + # Handle alert mode + if ($Settings.alert -eq $true) { + # Normalize the template list property based on what's passed + if ($Settings.'standards.SafeLinksTemplatePolicy.TemplateIds') { + $Settings | Add-Member -NotePropertyName 'TemplateList' -NotePropertyValue $Settings.'standards.SafeLinksTemplatePolicy.TemplateIds' -Force + } elseif ($Settings.TemplateIds) { + $Settings | Add-Member -NotePropertyName 'TemplateList' -NotePropertyValue $Settings.TemplateIds -Force + } + + if (-not $Settings.TemplateList) { + Write-StandardsAlert -message "SafeLinks templates could not be checked: No templates selected" -object $null -tenant $Tenant -standardName 'SafeLinksTemplatePolicy' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $Tenant -message "SafeLinks templates could not be checked: No templates selected" -sev Info + return + } + + $AllTemplatesApplied = $true + $AlertMessages = @() + + foreach ($Template in $Settings.TemplateList) { + $TemplateId = $Template.value + + # Get the template + $Table = Get-CippTable -tablename 'templates' + $Filter = "PartitionKey eq 'SafeLinksTemplate' and RowKey eq '$TemplateId'" + $Template = Get-CIPPAzDataTableEntity @Table -Filter $Filter + + if (-not $Template) { + $AlertMessages += "Template with ID $TemplateId not found" + $AllTemplatesApplied = $false + continue + } + + try { + $PolicyConfig = $Template.JSON | ConvertFrom-Json -ErrorAction Stop + $PolicyName = $PolicyConfig.PolicyName ?? $PolicyConfig.Name + $RuleName = $PolicyConfig.RuleName ?? $PolicyName + + # Check if policy and rule exist + $ExistingPoliciesParam = @{ + tenantid = $Tenant + cmdlet = 'Get-SafeLinksPolicy' + useSystemMailbox = $true + } + + $ExistingPolicies = New-ExoRequest @ExistingPoliciesParam + $PolicyExists = $ExistingPolicies | Where-Object { $_.Name -eq $PolicyName } + + $ExistingRulesParam = @{ + tenantid = $Tenant + cmdlet = 'Get-SafeLinksRule' + useSystemMailbox = $true + } + + $ExistingRules = New-ExoRequest @ExistingRulesParam + $RuleExists = $ExistingRules | Where-Object { $_.Name -eq $RuleName } + + if (-not $PolicyExists -or -not $RuleExists) { + $AllTemplatesApplied = $false + $Status = "SafeLinks template '$($PolicyConfig.Name)' is not applied" + + if (-not $PolicyExists) { + $Status += " - policy '$PolicyName' does not exist" + } + + if (-not $RuleExists) { + $Status += " - rule '$RuleName' does not exist" + } + + $AlertMessages += $Status + } + } + catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + $AlertMessages += "Failed to check template with ID $($TemplateId): $ErrorMessage" + $AllTemplatesApplied = $false + } + } + + if ($AllTemplatesApplied) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "All SafeLinks templates are correctly applied" -sev 'Info' + } + else { + $AlertMessage = "One or more SafeLinks templates are not correctly applied: " + ($AlertMessages -join " | ") + Write-StandardsAlert -message $AlertMessage -object @{ + Templates = $Settings.TemplateList + Issues = $AlertMessages + } -tenant $Tenant -standardName 'SafeLinksTemplatePolicy' -standardId $Settings.standardId + + Write-LogMessage -API 'Standards' -tenant $Tenant -message $AlertMessage -sev 'Info' + } + } + + # Handle report mode + if ($Settings.report -eq $true) { + # Normalize the template list property based on what's passed + if ($Settings.'standards.SafeLinksTemplatePolicy.TemplateIds') { + $Settings | Add-Member -NotePropertyName 'TemplateList' -NotePropertyValue $Settings.'standards.SafeLinksTemplatePolicy.TemplateIds' -Force + } elseif ($Settings.TemplateIds) { + $Settings | Add-Member -NotePropertyName 'TemplateList' -NotePropertyValue $Settings.TemplateIds -Force + } + + if (-not $Settings.TemplateList) { + Add-CIPPBPAField -FieldName 'SafeLinksTemplatePolicy' -FieldValue $false -StoreAs bool -Tenant $tenant + Set-CIPPStandardsCompareField -FieldName 'standards.SafeLinksTemplatePolicy' -FieldValue "No templates selected" -Tenant $Tenant + return + } + + $AllTemplatesApplied = $true + $ReportResults = @{} + + foreach ($Template in $Settings.TemplateList) { + $TemplateId = $Template.value + + # Get the template + $Table = Get-CippTable -tablename 'templates' + $Filter = "PartitionKey eq 'SafeLinksTemplate' and RowKey eq '$TemplateId'" + $Template = Get-CIPPAzDataTableEntity @Table -Filter $Filter + + if (-not $Template) { + $ReportResults[$TemplateId] = @{ + Success = $false + Message = "Template not found" + } + $AllTemplatesApplied = $false + continue + } + + try { + $PolicyConfig = $Template.JSON | ConvertFrom-Json -ErrorAction Stop + $PolicyName = $PolicyConfig.PolicyName ?? $PolicyConfig.Name + $RuleName = $PolicyConfig.RuleName ?? $PolicyName + + # Check if policy and rule exist + $ExistingPoliciesParam = @{ + tenantid = $Tenant + cmdlet = 'Get-SafeLinksPolicy' + useSystemMailbox = $true + } + + $ExistingPolicies = New-ExoRequest @ExistingPoliciesParam + $PolicyExists = $ExistingPolicies | Where-Object { $_.Name -eq $PolicyName } + + $ExistingRulesParam = @{ + tenantid = $Tenant + cmdlet = 'Get-SafeLinksRule' + useSystemMailbox = $true + } + + $ExistingRules = New-ExoRequest @ExistingRulesParam + $RuleExists = $ExistingRules | Where-Object { $_.Name -eq $RuleName } + + $ReportResults[$TemplateId] = @{ + Success = ($PolicyExists -and $RuleExists) + TemplateName = $PolicyConfig.Name + PolicyName = $PolicyName + RuleName = $RuleName + PolicyExists = $PolicyExists + RuleExists = $RuleExists + } + + if (-not $PolicyExists -or -not $RuleExists) { + $AllTemplatesApplied = $false + } + } + catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + $ReportResults[$TemplateId] = @{ + Success = $false + Message = $ErrorMessage + } + $AllTemplatesApplied = $false + } + } + + Add-CIPPBPAField -FieldName 'SafeLinksTemplatePolicy' -FieldValue $AllTemplatesApplied -StoreAs bool -Tenant $tenant + + if ($AllTemplatesApplied) { + Set-CIPPStandardsCompareField -FieldName 'standards.SafeLinksTemplatePolicy' -FieldValue $true -Tenant $Tenant + } + else { + Set-CIPPStandardsCompareField -FieldName 'standards.SafeLinksTemplatePolicy' -FieldValue @{ + TemplateResults = $ReportResults + ProcessedTemplates = $Settings.TemplateList.Count + SuccessfulTemplates = ($ReportResults.Values | Where-Object { $_.Success -eq $true }).Count + } -Tenant $Tenant + } + } +} diff --git a/openapi.json b/openapi.json index 7fb2383fa0cc..19763a14267d 100644 --- a/openapi.json +++ b/openapi.json @@ -8898,6 +8898,680 @@ } } }, + "/ExecDeleteSafeLinksPolicy": { + "get": { + "description": "ExecDeleteSafeLinksPolicy", + "summary": "ExecDeleteSafeLinksPolicy", + "tags": [ + "GET" + ], + "parameters": [ + { + "required": true, + "schema": { + "type": "string" + }, + "name": "tenantFilter", + "in": "query" + }, + { + "required": true, + "schema": { + "type": "string" + }, + "name": "RuleName", + "in": "query" + }, + { + "required": true, + "schema": { + "type": "string" + }, + "name": "PolicyName", + "in": "query" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": {}, + "type": "object" + } + } + }, + "description": "Successful operation" + } + } + } + }, + "/EditSafeLinksPolicy": { + "post": { + "description": "EditSafeLinksPolicy", + "summary": "EditSafeLinksPolicy", + "tags": [ + "POST" + ], + "parameters": [ + { + "required": true, + "schema": { + "type": "string" + }, + "name": "tenantFilter", + "in": "query" + }, + { + "required": true, + "schema": { + "type": "string" + }, + "name": "PolicyName", + "in": "query" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Name": { + "type": "string" + }, + "EnableSafeLinksForEmail": { + "type": "boolean" + }, + "EnableSafeLinksForTeams": { + "type": "boolean" + }, + "EnableSafeLinksForOffice": { + "type": "boolean" + }, + "TrackClicks": { + "type": "boolean" + }, + "AllowClickThrough": { + "type": "boolean" + }, + "ScanUrls": { + "type": "boolean" + }, + "EnableForInternalSenders": { + "type": "boolean" + }, + "DeliverMessageAfterScan": { + "type": "boolean" + }, + "DisableUrlRewrite": { + "type": "boolean" + }, + "DoNotRewriteUrls": { + "type": "array", + "items": { + "type": "string" + } + }, + "AdminDisplayName": { + "type": "string" + }, + "CustomNotificationText": { + "type": "string" + }, + "EnableOrganizationBranding": { + "type": "boolean" + }, + "Priority": { + "type": "integer" + }, + "Comments": { + "type": "string" + }, + "Enabled": { + "type": "boolean" + }, + "SentTo": { + "type": "array", + "items": { + "type": "string" + } + }, + "ExceptIfSentTo": { + "type": "array", + "items": { + "type": "string" + } + }, + "SentToMemberOf": { + "type": "array", + "items": { + "type": "string" + } + }, + "ExceptIfSentToMemberOf": { + "type": "array", + "items": { + "type": "string" + } + }, + "RecipientDomainIs": { + "type": "array", + "items": { + "type": "string" + } + }, + "ExceptIfRecipientDomainIs": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Results": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + "description": "Successful operation" + } + } + } + }, + "/ListSafeLinksPolicyDetails": { + "get": { + "description": "ListSafeLinksPolicyDetails", + "summary": "ListSafeLinksPolicyDetails", + "tags": [ + "GET" + ], + "parameters": [ + { + "required": true, + "schema": { + "type": "string" + }, + "name": "tenantFilter", + "in": "query" + }, + { + "required": true, + "schema": { + "type": "string" + }, + "name": "PolicyName", + "in": "query" + }, + { + "required": false, + "schema": { + "type": "string" + }, + "name": "RuleName", + "in": "query" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Results": { + "type": "object", + "properties": { + "Policy": { + "type": "object", + "properties": { + "Name": { + "type": "string" + }, + "EnableSafeLinksForEmail": { + "type": "boolean" + }, + "EnableSafeLinksForTeams": { + "type": "boolean" + }, + "EnableSafeLinksForOffice": { + "type": "boolean" + }, + "TrackClicks": { + "type": "boolean" + }, + "AllowClickThrough": { + "type": "boolean" + }, + "ScanUrls": { + "type": "boolean" + }, + "EnableForInternalSenders": { + "type": "boolean" + }, + "DeliverMessageAfterScan": { + "type": "boolean" + }, + "DisableUrlRewrite": { + "type": "boolean" + }, + "DoNotRewriteUrls": { + "type": "array", + "items": { + "type": "string" + } + }, + "AdminDisplayName": { + "type": "string" + }, + "CustomNotificationText": { + "type": "string" + }, + "EnableOrganizationBranding": { + "type": "boolean" + } + } + }, + "Rule": { + "type": "object", + "properties": { + "Name": { + "type": "string" + }, + "Priority": { + "type": "integer" + }, + "Comments": { + "type": "string" + }, + "State": { + "type": "string" + }, + "SentTo": { + "type": "array", + "items": { + "type": "string" + } + }, + "ExceptIfSentTo": { + "type": "array", + "items": { + "type": "string" + } + }, + "SentToMemberOf": { + "type": "array", + "items": { + "type": "string" + } + }, + "ExceptIfSentToMemberOf": { + "type": "array", + "items": { + "type": "string" + } + }, + "RecipientDomainIs": { + "type": "array", + "items": { + "type": "string" + } + }, + "ExceptIfRecipientDomainIs": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "Message": { + "type": "string" + } + } + } + } + } + } + }, + "description": "Successful operation" + } + } + } + }, + "/ExecNewSafeLinksPolicy": { + "post": { + "description": "Create a new SafeLinks policy and associated rule", + "summary": "Create SafeLinks Policy and Rule Configuration", + "tags": [ + "POST" + ], + "parameters": [ + { + "required": true, + "schema": { + "type": "string" + }, + "name": "tenantFilter", + "in": "query" + }, + { + "required": true, + "schema": { + "type": "string" + }, + "name": "Name", + "in": "query" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "EnableSafeLinksForEmail": { + "type": "boolean", + "description": "Enable Safe Links protection for email messages" + }, + "EnableSafeLinksForTeams": { + "type": "boolean", + "description": "Enable Safe Links protection for Teams messages" + }, + "EnableSafeLinksForOffice": { + "type": "boolean", + "description": "Enable Safe Links protection for Office applications" + }, + "TrackClicks": { + "type": "boolean", + "description": "Track user clicks related to Safe Links protection" + }, + "AllowClickThrough": { + "type": "boolean", + "description": "Allow users to click through to the original URL" + }, + "ScanUrls": { + "type": "boolean", + "description": "Enable real-time scanning of URLs" + }, + "EnableForInternalSenders": { + "type": "boolean", + "description": "Enable Safe Links for messages sent between internal senders" + }, + "DeliverMessageAfterScan": { + "type": "boolean", + "description": "Wait until URL scanning is complete before delivering the message" + }, + "DisableUrlRewrite": { + "type": "boolean", + "description": "Disable the rewriting of URLs in messages" + }, + "DoNotRewriteUrls": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of URLs that will not be rewritten by Safe Links" + }, + "AdminDisplayName": { + "type": "string", + "description": "Display name for the policy in the admin interface" + }, + "CustomNotificationText": { + "type": "string", + "description": "Custom text for the notification when a link is blocked" + }, + "EnableOrganizationBranding": { + "type": "boolean", + "description": "Enable organization branding on warning pages" + }, + "Priority": { + "type": "integer", + "description": "Priority of the rule (lower numbers = higher priority)" + }, + "Comments": { + "type": "string", + "description": "Administrative comments for the rule" + }, + "Enabled": { + "type": "boolean", + "description": "Whether the rule is enabled or disabled" + }, + "SentTo": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Apply the rule if the recipient is any of these email addresses" + }, + "SentToMemberOf": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Apply the rule if the recipient is a member of any of these groups" + }, + "RecipientDomainIs": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Apply the rule if the recipient's domain matches any of these domains" + }, + "ExceptIfSentTo": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Do not apply the rule if the recipient is any of these email addresses" + }, + "ExceptIfSentToMemberOf": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Do not apply the rule if the recipient is a member of any of these groups" + }, + "ExceptIfRecipientDomainIs": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Do not apply the rule if the recipient's domain matches any of these domains" + } + } + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Results": { + "type": "string", + "description": "Result message from the operation" + } + } + } + } + }, + "description": "Successfully created SafeLinks policy and rule" + }, + "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" + } + } + } + } + } + } + } + } + }, + "/ListSafeLinksPolicyTemplates": { + "get": { + "description": "List SafeLinks Policy Templates", + "summary": "List SafeLinks Policy Templates", + "tags": [ + "GET" + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": {} + } + } + } + }, + "description": "Successful operation" + } + } + } + }, + "/RemoveSafeLinksPolicyTemplate": { + "get": { + "description": "Remove SafeLinks Policy Template", + "summary": "Remove SafeLinks Policy Template", + "tags": [ + "GET" + ], + "parameters": [ + { + "required": true, + "schema": { + "type": "string" + }, + "name": "id", + "in": "query" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": {}, + "type": "object" + } + } + }, + "description": "Successful operation" + } + } + } + }, + "/AddSafeLinksPolicyTemplate": { + "post": { + "description": "Add SafeLinks Policy Template", + "summary": "Add SafeLinks Policy Template", + "tags": [ + "POST" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {} + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": {}, + "type": "object" + } + } + }, + "description": "Successful operation" + } + } + } + }, + "/AddSafeLinksPolicyFromTemplate": { + "post": { + "description": "Deploy SafeLinks Policy From Template", + "summary": "Deploy SafeLinks Policy From Template", + "tags": [ + "POST" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {} + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": {}, + "type": "object" + } + } + }, + "description": "Successful operation" + } + } + } + }, "openapi": "3.1.0", "servers": [ { From 9026bc0d3c1916993e407cadaf5a03870b6d4ca0 Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 3 Jun 2025 08:48:36 +0800 Subject: [PATCH 023/160] Round 2 --- Config/standards.json | 2 +- .../Invoke-AddSafeLinksPolicyFromTemplate.ps1 | 324 ++++--- .../Invoke-ExecNewSafeLinksPolicy.ps1 | 49 +- .../Invoke-ListSafeLinksPolicy.ps1 | 124 ++- ...ke-CIPPStandardSafeLinksTemplatePolicy.ps1 | 793 ++++++++---------- 5 files changed, 666 insertions(+), 626 deletions(-) diff --git a/Config/standards.json b/Config/standards.json index 0be93153192d..dcc0335ddb71 100644 --- a/Config/standards.json +++ b/Config/standards.json @@ -1744,7 +1744,7 @@ "label": "Select SafeLinks Policy Templates", "api": { "url": "/api/ListSafeLinksPolicyTemplates", - "labelField": "name", + "labelField": "TemplateName", "valueField": "GUID", "queryKey": "ListSafeLinksPolicyTemplates" } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyFromTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyFromTemplate.ps1 index e6e1c90824fc..5512afc6544a 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyFromTemplate.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyFromTemplate.ps1 @@ -7,7 +7,7 @@ Function Invoke-AddSafeLinksPolicyFromTemplate { .ROLE Exchange.SafeLinks.ReadWrite .DESCRIPTION - This function deploys a SafeLinks policy and rule from a template to selected tenants. + This function deploys SafeLinks policies and rules from templates to selected tenants. #> [CmdletBinding()] param($Request, $TriggerMetadata) @@ -19,204 +19,194 @@ Function Invoke-AddSafeLinksPolicyFromTemplate { try { $RequestBody = $Request.Body - # Extract tenant IDs from the selectedTenants objects - just get the value property + # Extract tenant IDs from selectedTenants $SelectedTenants = $RequestBody.selectedTenants | ForEach-Object { $_.value } - if ('AllTenants' -in $SelectedTenants) { $SelectedTenants = (Get-Tenants).defaultDomainName } + if ('AllTenants' -in $SelectedTenants) { + $SelectedTenants = (Get-Tenants).defaultDomainName + } + + # Extract templates from TemplateList + $Templates = $RequestBody.TemplateList | ForEach-Object { $_.value } - # Parse the PolicyConfig if it's a string - if ($RequestBody.PolicyConfig -is [string]) { - $PolicyConfig = $RequestBody.PolicyConfig | ConvertFrom-Json -ErrorAction Stop - } else { - $PolicyConfig = $RequestBody.PolicyConfig + if (-not $Templates -or $Templates.Count -eq 0) { + throw "No templates provided in TemplateList" } - # Helper function to process array fields - function Process-ArrayField { - param ( - [Parameter(Mandatory = $false)] - $Field - ) + # Helper function to process array fields with cleaner logic + function ConvertTo-SafeArray { + param($Field) if ($null -eq $Field) { return @() } - # If already an array, process each item + # Handle arrays if ($Field -is [array]) { - $result = @() - foreach ($item in $Field) { - if ($item -is [string]) { - $result += $item - } - elseif ($item -is [hashtable] -or $item -is [PSCustomObject]) { - # Extract value from object - if ($null -ne $item.value) { - $result += $item.value - } - elseif ($null -ne $item.userPrincipalName) { - $result += $item.userPrincipalName - } - elseif ($null -ne $item.id) { - $result += $item.id - } - else { - $result += $item.ToString() - } - } - else { - $result += $item.ToString() - } + return $Field | ForEach-Object { + if ($_ -is [string]) { $_ } + elseif ($_.value) { $_.value } + elseif ($_.userPrincipalName) { $_.userPrincipalName } + elseif ($_.id) { $_.id } + else { $_.ToString() } } - return $result } - # If it's a single object + # Handle single objects if ($Field -is [hashtable] -or $Field -is [PSCustomObject]) { - if ($null -ne $Field.value) { return @($Field.value) } - if ($null -ne $Field.userPrincipalName) { return @($Field.userPrincipalName) } - if ($null -ne $Field.id) { return @($Field.id) } + if ($Field.value) { return @($Field.value) } + if ($Field.userPrincipalName) { return @($Field.userPrincipalName) } + if ($Field.id) { return @($Field.id) } } - # If it's a string, return as an array with one item - if ($Field -is [string]) { - return @($Field) - } + # Handle strings + if ($Field -is [string]) { return @($Field) } return @($Field) } - $Results = foreach ($TenantFilter in $SelectedTenants) { - try { - # Extract policy name from template - $PolicyName = $PolicyConfig.PolicyName ?? $PolicyConfig.Name - $RuleName = $PolicyConfig.RuleName ?? $PolicyName - - # Check if policy exists by listing all policies and filtering - $ExistingPoliciesParam = @{ - tenantid = $TenantFilter - cmdlet = 'Get-SafeLinksPolicy' - useSystemMailbox = $true - } + function Test-PolicyExists { + param($TenantFilter, $PolicyName) - $ExistingPolicies = New-ExoRequest @ExistingPoliciesParam - $PolicyExists = $ExistingPolicies | Where-Object { $_.Name -eq $PolicyName } + $ExistingPolicies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-SafeLinksPolicy' -useSystemMailbox $true + return $ExistingPolicies | Where-Object { $_.Name -eq $PolicyName } + } - if ($PolicyExists) { - Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Policy with name '$PolicyName' already exists in tenant $TenantFilter" -Sev 'Warning' - "Policy with name '$PolicyName' already exists in tenant $TenantFilter" - continue - } + function Test-RuleExists { + param($TenantFilter, $RuleName) - # Check if rule exists by listing all rules and filtering - $ExistingRulesParam = @{ - tenantid = $TenantFilter - cmdlet = 'Get-SafeLinksRule' - useSystemMailbox = $true - } + $ExistingRules = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-SafeLinksRule' -useSystemMailbox $true + return $ExistingRules | Where-Object { $_.Name -eq $RuleName } + } - $ExistingRules = New-ExoRequest @ExistingRulesParam - $RuleExists = $ExistingRules | Where-Object { $_.Name -eq $RuleName } + function New-SafeLinksPolicyFromTemplate { + param($TenantFilter, $Template) - if ($RuleExists) { - Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Rule with name '$RuleName' already exists in tenant $TenantFilter" -Sev 'Warning' - "Rule with name '$RuleName' already exists in tenant $TenantFilter" - continue - } + $PolicyName = $Template.PolicyName + $RuleName = $Template.RuleName ?? "$($PolicyName)_Rule" - # Process arrays in the template - $DoNotRewriteUrls = Process-ArrayField -Field $PolicyConfig.DoNotRewriteUrls - $SentTo = Process-ArrayField -Field $PolicyConfig.SentTo - $SentToMemberOf = Process-ArrayField -Field $PolicyConfig.SentToMemberOf - $RecipientDomainIs = Process-ArrayField -Field $PolicyConfig.RecipientDomainIs - $ExceptIfSentTo = Process-ArrayField -Field $PolicyConfig.ExceptIfSentTo - $ExceptIfSentToMemberOf = Process-ArrayField -Field $PolicyConfig.ExceptIfSentToMemberOf - $ExceptIfRecipientDomainIs = Process-ArrayField -Field $PolicyConfig.ExceptIfRecipientDomainIs - - # PART 1: Create SafeLinks Policy - # Build command parameters for policy - $policyParams = @{ - Name = $PolicyName - } + # Check if policy already exists + if (Test-PolicyExists -TenantFilter $TenantFilter -PolicyName $PolicyName) { + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Policy '$PolicyName' already exists" -Sev 'Warning' + return "Policy '$PolicyName' already exists in tenant $TenantFilter" + } + + # Check if rule already exists + if (Test-RuleExists -TenantFilter $TenantFilter -RuleName $RuleName) { + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Rule '$RuleName' already exists" -Sev 'Warning' + return "Rule '$RuleName' already exists in tenant $TenantFilter" + } + + # Process array fields + $DoNotRewriteUrls = ConvertTo-SafeArray -Field $Template.DoNotRewriteUrls + $SentTo = ConvertTo-SafeArray -Field $Template.SentTo + $SentToMemberOf = ConvertTo-SafeArray -Field $Template.SentToMemberOf + $RecipientDomainIs = ConvertTo-SafeArray -Field $Template.RecipientDomainIs + $ExceptIfSentTo = ConvertTo-SafeArray -Field $Template.ExceptIfSentTo + $ExceptIfSentToMemberOf = ConvertTo-SafeArray -Field $Template.ExceptIfSentToMemberOf + $ExceptIfRecipientDomainIs = ConvertTo-SafeArray -Field $Template.ExceptIfRecipientDomainIs + + # Create policy parameters + $PolicyParams = @{ Name = $PolicyName } + + # Policy configuration mapping + $PolicyMappings = @{ + 'EnableSafeLinksForEmail' = 'EnableSafeLinksForEmail' + 'EnableSafeLinksForTeams' = 'EnableSafeLinksForTeams' + 'EnableSafeLinksForOffice' = 'EnableSafeLinksForOffice' + 'TrackClicks' = 'TrackClicks' + 'AllowClickThrough' = 'AllowClickThrough' + 'ScanUrls' = 'ScanUrls' + 'EnableForInternalSenders' = 'EnableForInternalSenders' + 'DeliverMessageAfterScan' = 'DeliverMessageAfterScan' + 'DisableUrlRewrite' = 'DisableUrlRewrite' + 'AdminDisplayName' = 'AdminDisplayName' + 'CustomNotificationText' = 'CustomNotificationText' + 'EnableOrganizationBranding' = 'EnableOrganizationBranding' + } - # Only add parameters that are explicitly provided in the template - if ($null -ne $PolicyConfig.EnableSafeLinksForEmail) { $policyParams.Add('EnableSafeLinksForEmail', $PolicyConfig.EnableSafeLinksForEmail) } - if ($null -ne $PolicyConfig.EnableSafeLinksForTeams) { $policyParams.Add('EnableSafeLinksForTeams', $PolicyConfig.EnableSafeLinksForTeams) } - if ($null -ne $PolicyConfig.EnableSafeLinksForOffice) { $policyParams.Add('EnableSafeLinksForOffice', $PolicyConfig.EnableSafeLinksForOffice) } - if ($null -ne $PolicyConfig.TrackClicks) { $policyParams.Add('TrackClicks', $PolicyConfig.TrackClicks) } - if ($null -ne $PolicyConfig.AllowClickThrough) { $policyParams.Add('AllowClickThrough', $PolicyConfig.AllowClickThrough) } - if ($null -ne $PolicyConfig.ScanUrls) { $policyParams.Add('ScanUrls', $PolicyConfig.ScanUrls) } - if ($null -ne $PolicyConfig.EnableForInternalSenders) { $policyParams.Add('EnableForInternalSenders', $PolicyConfig.EnableForInternalSenders) } - if ($null -ne $PolicyConfig.DeliverMessageAfterScan) { $policyParams.Add('DeliverMessageAfterScan', $PolicyConfig.DeliverMessageAfterScan) } - if ($null -ne $PolicyConfig.DisableUrlRewrite) { $policyParams.Add('DisableUrlRewrite', $PolicyConfig.DisableUrlRewrite) } - if ($null -ne $DoNotRewriteUrls -and $DoNotRewriteUrls.Count -gt 0) { $policyParams.Add('DoNotRewriteUrls', $DoNotRewriteUrls) } - if ($null -ne $PolicyConfig.AdminDisplayName) { $policyParams.Add('AdminDisplayName', $PolicyConfig.AdminDisplayName) } - if ($null -ne $PolicyConfig.CustomNotificationText) { $policyParams.Add('CustomNotificationText', $PolicyConfig.CustomNotificationText) } - if ($null -ne $PolicyConfig.EnableOrganizationBranding) { $policyParams.Add('EnableOrganizationBranding', $PolicyConfig.EnableOrganizationBranding) } - - $ExoPolicyRequestParam = @{ - tenantid = $TenantFilter - cmdlet = 'New-SafeLinksPolicy' - cmdParams = $policyParams - useSystemMailbox = $true + foreach ($templateKey in $PolicyMappings.Keys) { + if ($null -ne $Template.$templateKey) { + $PolicyParams[$PolicyMappings[$templateKey]] = $Template.$templateKey } + } - $null = New-ExoRequest @ExoPolicyRequestParam - $PolicyResult = "Successfully created new SafeLinks policy '$PolicyName'" - Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $PolicyResult -Sev 'Info' + if ($DoNotRewriteUrls.Count -gt 0) { + $PolicyParams['DoNotRewriteUrls'] = $DoNotRewriteUrls + } + + # Create SafeLinks Policy + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'New-SafeLinksPolicy' -cmdParams $PolicyParams -useSystemMailbox $true + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Created SafeLinks policy '$PolicyName'" -Sev 'Info' + + # Create rule parameters + $RuleParams = @{ + Name = $RuleName + SafeLinksPolicy = $PolicyName + } - # PART 2: Create SafeLinks Rule - # Build command parameters for rule - $ruleParams = @{ - Name = $RuleName - SafeLinksPolicy = $PolicyName + # Rule configuration mapping + $RuleMappings = @{ + 'Priority' = 'Priority' + 'TemplateDescription' = 'Comments' + } + + foreach ($templateKey in $RuleMappings.Keys) { + if ($null -ne $Template.$templateKey) { + $RuleParams[$RuleMappings[$templateKey]] = $Template.$templateKey } + } + + # Add array parameters if they have values + $ArrayMappings = @{ + 'SentTo' = $SentTo + 'SentToMemberOf' = $SentToMemberOf + 'RecipientDomainIs' = $RecipientDomainIs + 'ExceptIfSentTo' = $ExceptIfSentTo + 'ExceptIfSentToMemberOf' = $ExceptIfSentToMemberOf + 'ExceptIfRecipientDomainIs' = $ExceptIfRecipientDomainIs + } - # Only add parameters that are explicitly provided - if ($null -ne $PolicyConfig.Priority) { $ruleParams.Add('Priority', $PolicyConfig.Priority) } - if ($null -ne $PolicyConfig.Description) { $ruleParams.Add('Comments', $PolicyConfig.Description) } - if ($null -ne $SentTo -and $SentTo.Count -gt 0) { $ruleParams.Add('SentTo', $SentTo) } - if ($null -ne $SentToMemberOf -and $SentToMemberOf.Count -gt 0) { $ruleParams.Add('SentToMemberOf', $SentToMemberOf) } - if ($null -ne $RecipientDomainIs -and $RecipientDomainIs.Count -gt 0) { $ruleParams.Add('RecipientDomainIs', $RecipientDomainIs) } - if ($null -ne $ExceptIfSentTo -and $ExceptIfSentTo.Count -gt 0) { $ruleParams.Add('ExceptIfSentTo', $ExceptIfSentTo) } - if ($null -ne $ExceptIfSentToMemberOf -and $ExceptIfSentToMemberOf.Count -gt 0) { $ruleParams.Add('ExceptIfSentToMemberOf', $ExceptIfSentToMemberOf) } - if ($null -ne $ExceptIfRecipientDomainIs -and $ExceptIfRecipientDomainIs.Count -gt 0) { $ruleParams.Add('ExceptIfRecipientDomainIs', $ExceptIfRecipientDomainIs) } - - $ExoRuleRequestParam = @{ - tenantid = $TenantFilter - cmdlet = 'New-SafeLinksRule' - cmdParams = $ruleParams - useSystemMailbox = $true + foreach ($paramName in $ArrayMappings.Keys) { + if ($ArrayMappings[$paramName].Count -gt 0) { + $RuleParams[$paramName] = $ArrayMappings[$paramName] } + } - $null = New-ExoRequest @ExoRuleRequestParam - $RuleResult = "Successfully created new SafeLinks rule '$RuleName'" - Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $RuleResult -Sev 'Info' - - # If State is specified in the template, enable or disable the rule - if ($null -ne $PolicyConfig.State) { - $Enabled = $PolicyConfig.State -eq "Enabled" - $EnableCmdlet = $Enabled ? 'Enable-SafeLinksRule' : 'Disable-SafeLinksRule' - $EnableRequestParam = @{ - tenantid = $TenantFilter - cmdlet = $EnableCmdlet - cmdParams = @{ - Identity = $RuleName - } - useSystemMailbox = $true - } - - $null = New-ExoRequest @EnableRequestParam - $StateMsg = $Enabled ? "enabled" : "disabled" + # Create SafeLinks Rule + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'New-SafeLinksRule' -cmdParams $RuleParams -useSystemMailbox $true + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Created SafeLinks rule '$RuleName'" -Sev 'Info' + + # Handle rule state + $StateMessage = "" + if ($null -ne $Template.State) { + $IsEnabled = switch ($Template.State) { + "Enabled" { $true } + "Disabled" { $false } + $true { $true } + $false { $false } + default { $null } } - # Return success message as a simple string - "Successfully deployed SafeLinks policy '$PolicyName' and rule '$RuleName' to tenant $TenantFilter" + $(if ($null -ne $PolicyConfig.State) { " (rule $StateMsg)" } else { "" }) + if ($null -ne $IsEnabled) { + $Cmdlet = $IsEnabled ? 'Enable-SafeLinksRule' : 'Disable-SafeLinksRule' + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet $Cmdlet -cmdParams @{ Identity = $RuleName } -useSystemMailbox $true + $StateMessage = " (rule $($IsEnabled ? 'enabled' : 'disabled'))" + } } - catch { - $ErrorMessage = Get-CippException -Exception $_ - $ErrorDetail = "Failed to deploy SafeLinks policy template to tenant $TenantFilter. Error: $($ErrorMessage.NormalizedError)" - Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $ErrorDetail -Sev 'Error' - # Return error message as a simple string - "Failed to deploy SafeLinks policy template to tenant $TenantFilter. Error: $($ErrorMessage.NormalizedError)" + return "Successfully deployed SafeLinks policy '$PolicyName' and rule '$RuleName' to tenant $TenantFilter$StateMessage" + } + + # Process each tenant and template combination + $Results = foreach ($TenantFilter in $SelectedTenants) { + foreach ($Template in $Templates) { + try { + New-SafeLinksPolicyFromTemplate -TenantFilter $TenantFilter -Template $Template + } + catch { + $ErrorMessage = Get-CippException -Exception $_ + $ErrorDetail = "Failed to deploy template '$($Template.TemplateName)' to tenant $TenantFilter. Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $ErrorDetail -Sev 'Error' + $ErrorDetail + } } } @@ -229,9 +219,9 @@ Function Invoke-AddSafeLinksPolicyFromTemplate { $StatusCode = [HttpStatusCode]::InternalServerError } - # Associate values to output bindings by calling 'Push-OutputBinding'. + # Return response Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ - StatusCode = $StatusCode - Body = @{Results = $Results} - }) + StatusCode = $StatusCode + Body = @{ Results = $Results } + }) } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecNewSafeLinksPolicy.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecNewSafeLinksPolicy.ps1 index 2a29deecc7dc..94db830cba75 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecNewSafeLinksPolicy.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecNewSafeLinksPolicy.ps1 @@ -49,6 +49,18 @@ function Invoke-ExecNewSafeLinksPolicy { $ExceptIfSentToMemberOf = $Request.Body.ExceptIfSentToMemberOf $ExceptIfRecipientDomainIs = $Request.Body.ExceptIfRecipientDomainIs + function Test-PolicyExists { + param($TenantFilter, $PolicyName) + $ExistingPolicies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-SafeLinksPolicy' -useSystemMailbox $true + return $ExistingPolicies | Where-Object { $_.Name -eq $PolicyName } + } + + function Test-RuleExists { + param($TenantFilter, $RuleName) + $ExistingRules = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-SafeLinksRule' -useSystemMailbox $true + return $ExistingRules | Where-Object { $_.Name -eq $RuleName } + } + # Helper function to process array fields function Process-ArrayField { param ( @@ -112,38 +124,18 @@ function Invoke-ExecNewSafeLinksPolicy { $DoNotRewriteUrls = Process-ArrayField -Field $DoNotRewriteUrls try { - # Check if policy exists by listing all policies and filtering - $ExistingPoliciesParam = @{ - tenantid = $TenantFilter - cmdlet = 'Get-SafeLinksPolicy' - useSystemMailbox = $true + # Check if policy already exists + if (Test-PolicyExists -TenantFilter $TenantFilter -PolicyName $PolicyName) { + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Policy '$PolicyName' already exists" -Sev 'Warning' + return "Policy '$PolicyName' already exists in tenant $TenantFilter" } - $ExistingPolicies = New-ExoRequest @ExistingPoliciesParam - $PolicyExists = $ExistingPolicies | Where-Object { $_.Name -eq $PolicyName } - - if ($PolicyExists) { - Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Policy with name '$PolicyName' already exists in tenant $TenantFilter" -Sev 'Warning' - "Policy with name '$PolicyName' already exists in tenant $TenantFilter" - continue + # Check if rule already exists + if (Test-RuleExists -TenantFilter $TenantFilter -RuleName $RuleName) { + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Rule '$RuleName' already exists" -Sev 'Warning' + return "Rule '$RuleName' already exists in tenant $TenantFilter" } - # Check if rule exists by listing all rules and filtering - $ExistingRulesParam = @{ - tenantid = $TenantFilter - cmdlet = 'Get-SafeLinksRule' - useSystemMailbox = $true - } - - $ExistingRules = New-ExoRequest @ExistingRulesParam - $RuleExists = $ExistingRules | Where-Object { $_.Name -eq $RuleName } - - if ($RuleExists) { - Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Rule with name '$RuleName' already exists in tenant $TenantFilter" -Sev 'Warning' - "Rule with name '$RuleName' already exists in tenant $TenantFilter" - continue - } - # PART 1: Create SafeLinks Policy # Build command parameters for policy $policyParams = @{ Name = $PolicyName @@ -175,7 +167,6 @@ function Invoke-ExecNewSafeLinksPolicy { $PolicyResult = "Successfully created new SafeLinks policy '$PolicyName'" Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $PolicyResult -Sev 'Info' - # PART 2: Create SafeLinks Rule # Build command parameters for rule $ruleParams = @{ Name = $RuleName diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicy.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicy.ps1 index 23a1a9c2a6b9..a3a347e6526e 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicy.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicy.ps1 @@ -6,7 +6,7 @@ Function Invoke-ListSafeLinksPolicy { .ROLE Security.SafeLinksPolicy.Read .DESCRIPTION - This function is used to list the Safe Links policies in the tenant. + This function is used to list the Safe Links policies in the tenant, including unmatched rules and policies. #> [CmdletBinding()] param($Request, $TriggerMetadata) @@ -20,15 +20,23 @@ Function Invoke-ListSafeLinksPolicy { $Rules = New-ExoRequest -tenantid $Tenantfilter -cmdlet 'Get-SafeLinksRule' | Select-Object -Property * -ExcludeProperty '*@odata.type' , '*@data.type' $BuiltInRules = New-ExoRequest -tenantid $Tenantfilter -cmdlet 'Get-EOPProtectionPolicyRule' | Select-Object -Property * -ExcludeProperty '*@odata.type' , '*@data.type' - # Single-pass processing optimized for Azure Functions - $Output = foreach ($policy in $Policies) { + # Track matched items to identify orphans + $MatchedRules = [System.Collections.Generic.HashSet[string]]::new() + $MatchedPolicies = [System.Collections.Generic.HashSet[string]]::new() + $MatchedBuiltInRules = [System.Collections.Generic.HashSet[string]]::new() + $Output = [System.Collections.Generic.List[PSCustomObject]]::new() + + # First pass: Process policies with their associated rules + foreach ($policy in $Policies) { $policyName = $policy.Name + $MatchedPolicies.Add($policyName) | Out-Null # Find associated rule (single lookup per policy) $associatedRule = $null foreach ($rule in $Rules) { if ($rule.SafeLinksPolicy -eq $policyName) { $associatedRule = $rule + $MatchedRules.Add($rule.Name) | Out-Null break } } @@ -38,12 +46,13 @@ Function Invoke-ListSafeLinksPolicy { foreach ($builtInRule in $BuiltInRules) { if ($policyName -like "$($builtInRule.Name)*") { $matchingBuiltInRule = $builtInRule + $MatchedBuiltInRules.Add($builtInRule.Name) | Out-Null break } } - # Create output object with all properties in one go - [PSCustomObject]@{ + # Create output object for matched policy + $OutputItem = [PSCustomObject]@{ # Copy all original policy properties Name = $policy.Name AdminDisplayName = $policy.AdminDisplayName @@ -73,22 +82,121 @@ Function Invoke-ListSafeLinksPolicy { ExceptIfRecipientDomainIs = $associatedRule.ExceptIfRecipientDomainIs Description = $policy.AdminDisplayName IsBuiltIn = ($matchingBuiltInRule -ne $null) + ConfigurationStatus = if ($associatedRule) { "Complete" } else { "Policy Only (Missing Rule)" } + } + $Output.Add($OutputItem) + } + + # Second pass: Add unmatched rules (orphaned rules without policies) + foreach ($rule in $Rules) { + if (-not $MatchedRules.Contains($rule.Name)) { + # This rule doesn't have a matching policy + $OutputItem = [PSCustomObject]@{ + # Policy properties (null since no policy exists) + Name = $null + AdminDisplayName = $null + EnableSafeLinksForEmail = $null + EnableSafeLinksForTeams = $null + EnableSafeLinksForOffice = $null + TrackClicks = $null + AllowClickThrough = $null + ScanUrls = $null + EnableForInternalSenders = $null + DeliverMessageAfterScan = $null + DisableUrlRewrite = $null + DoNotRewriteUrls = $null + CustomNotificationText = $null + EnableOrganizationBranding = $null + + # Rule properties + PolicyName = $rule.SafeLinksPolicy + RuleName = $rule.Name + Priority = $rule.Priority + State = $rule.State + SentTo = $rule.SentTo + SentToMemberOf = $rule.SentToMemberOf + RecipientDomainIs = $rule.RecipientDomainIs + ExceptIfSentTo = $rule.ExceptIfSentTo + ExceptIfSentToMemberOf = $rule.ExceptIfSentToMemberOf + ExceptIfRecipientDomainIs = $rule.ExceptIfRecipientDomainIs + Description = $rule.Comments + IsBuiltIn = $false + ConfigurationStatus = "Rule Only (Missing Policy: $($rule.SafeLinksPolicy))" + } + $Output.Add($OutputItem) + } + } + + # Third pass: Add unmatched built-in rules + foreach ($builtInRule in $BuiltInRules) { + if (-not $MatchedBuiltInRules.Contains($builtInRule.Name)) { + # Check if this built-in rule might be SafeLinks related + if ($builtInRule.Name -like "*SafeLinks*" -or $builtInRule.Name -like "*Safe*Links*") { + $OutputItem = [PSCustomObject]@{ + # Policy properties (null since no policy exists) + Name = $null + AdminDisplayName = $null + EnableSafeLinksForEmail = $null + EnableSafeLinksForTeams = $null + EnableSafeLinksForOffice = $null + TrackClicks = $null + AllowClickThrough = $null + ScanUrls = $null + EnableForInternalSenders = $null + DeliverMessageAfterScan = $null + DisableUrlRewrite = $null + DoNotRewriteUrls = $null + CustomNotificationText = $null + EnableOrganizationBranding = $null + + # Built-in rule properties + PolicyName = $null + RuleName = $builtInRule.Name + Priority = $builtInRule.Priority + State = $builtInRule.State + SentTo = $builtInRule.SentTo + SentToMemberOf = $builtInRule.SentToMemberOf + RecipientDomainIs = $builtInRule.RecipientDomainIs + ExceptIfSentTo = $builtInRule.ExceptIfSentTo + ExceptIfSentToMemberOf = $builtInRule.ExceptIfSentToMemberOf + ExceptIfRecipientDomainIs = $builtInRule.ExceptIfRecipientDomainIs + Description = $builtInRule.Comments + IsBuiltIn = $true + ConfigurationStatus = "Built-In Rule Only (No Associated Policy)" + } + $Output.Add($OutputItem) + } } } - Write-LogMessage -headers $Headers -API $APIName -message "Retrieved $($Output.Count) Safe Links policies" -Sev 'Info' + # Sort output by ConfigurationStatus and Name for better organization + $SortedOutput = $Output.ToArray() | Sort-Object ConfigurationStatus, Name, RuleName + + # Generate summary statistics + $CompleteConfigs = ($SortedOutput | Where-Object { $_.ConfigurationStatus -eq "Complete" }).Count + $PolicyOnlyConfigs = ($SortedOutput | Where-Object { $_.ConfigurationStatus -like "*Policy Only*" }).Count + $RuleOnlyConfigs = ($SortedOutput | Where-Object { $_.ConfigurationStatus -like "*Rule Only*" }).Count + $BuiltInOnlyConfigs = ($SortedOutput | Where-Object { $_.ConfigurationStatus -like "*Built-In Rule Only*" }).Count + + Write-LogMessage -headers $Headers -API $APIName -message "Retrieved SafeLinks configurations: $CompleteConfigs complete, $PolicyOnlyConfigs policy-only, $RuleOnlyConfigs rule-only, $BuiltInOnlyConfigs built-in-only" -Sev 'Info' + + if ($PolicyOnlyConfigs -gt 0 -or $RuleOnlyConfigs -gt 0) { + Write-LogMessage -headers $Headers -API $APIName -message "Found $($PolicyOnlyConfigs + $RuleOnlyConfigs) orphaned SafeLinks configurations that may need attention" -Sev 'Warning' + } + $StatusCode = [HttpStatusCode]::OK + $FinalOutput = $SortedOutput } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -headers $Headers -API $APIName -message "Error retrieving Safe Links policies: $ErrorMessage" -Sev 'Error' $StatusCode = [HttpStatusCode]::Forbidden - $Output = $ErrorMessage + $FinalOutput = $ErrorMessage } # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ StatusCode = $StatusCode - Body = $Output + Body = $FinalOutput }) } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksTemplatePolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksTemplatePolicy.ps1 index 84c654359d2e..e630e12a900b 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksTemplatePolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksTemplatePolicy.ps1 @@ -31,508 +31,459 @@ function Invoke-CIPPStandardSafeLinksTemplatePolicy { #> param($Tenant, $Settings) - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'SafeLinksPolicy' Write-LogMessage -API 'Standards' -tenant $Tenant -message "Processing SafeLinks template with settings: $($Settings | ConvertTo-Json -Compress)" -sev Debug # Verify tenant has necessary license + if (-not (Test-MDOLicense -Tenant $Tenant -Settings $Settings)) { + return + } + + # Normalize template list property + $TemplateList = Get-NormalizedTemplateList -Settings $Settings + if (-not $TemplateList) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "No templates selected for SafeLinks policy deployment" -sev Error + return + } + + # Handle different modes + switch ($true) { + ($Settings.remediate -eq $true) { + Invoke-SafeLinksRemediation -Tenant $Tenant -TemplateList $TemplateList -Settings $Settings + } + ($Settings.alert -eq $true) { + Invoke-SafeLinksAlert -Tenant $Tenant -TemplateList $TemplateList -Settings $Settings + } + ($Settings.report -eq $true) { + Invoke-SafeLinksReport -Tenant $Tenant -TemplateList $TemplateList -Settings $Settings + } + } +} + +function Test-MDOLicense { + param($Tenant, $Settings) + $ServicePlans = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/subscribedSkus?$select=servicePlans' -tenantid $Tenant $ServicePlans = $ServicePlans.servicePlans.servicePlanName $MDOLicensed = $ServicePlans -contains 'ATP_ENTERPRISE' if (-not $MDOLicensed) { + $Message = 'Tenant does not have Microsoft Defender for Office 365 license' + if ($Settings.remediate -eq $true) { - Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Failed to apply SafeLinks templates: Tenant does not have Microsoft Defender for Office 365 license' -sev Error + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to apply SafeLinks templates: $Message" -sev Error } if ($Settings.alert -eq $true) { - Write-StandardsAlert -message 'SafeLinks templates could not be applied: Tenant does not have Microsoft Defender for Office 365 license' -object $MDOLicensed -tenant $Tenant -standardName 'SafeLinksTemplatePolicy' -standardId $Settings.standardId - Write-LogMessage -API 'Standards' -tenant $Tenant -message 'SafeLinks templates could not be applied: Tenant does not have Microsoft Defender for Office 365 license' -sev Info + Write-StandardsAlert -message "SafeLinks templates could not be applied: $Message" -object $MDOLicensed -tenant $Tenant -standardName 'SafeLinksTemplatePolicy' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $Tenant -message "SafeLinks templates could not be applied: $Message" -sev Info } if ($Settings.report -eq $true) { - Add-CIPPBPAField -FieldName 'SafeLinksTemplatePolicy' -FieldValue $false -StoreAs bool -Tenant $tenant + Add-CIPPBPAField -FieldName 'SafeLinksTemplatePolicy' -FieldValue $false -StoreAs bool -Tenant $Tenant Set-CIPPStandardsCompareField -FieldName 'standards.SafeLinksTemplatePolicy' -FieldValue $false -Tenant $Tenant } - return + return $false } - # Handle remediation - If ($Settings.remediate -eq $true) { - # Normalize the template list property based on what's passed - support multiple possible formats - if ($Settings.'standards.SafeLinksTemplatePolicy.TemplateIds') { - $Settings | Add-Member -NotePropertyName 'TemplateList' -NotePropertyValue $Settings.'standards.SafeLinksTemplatePolicy.TemplateIds' -Force - } elseif ($Settings.TemplateIds) { - $Settings | Add-Member -NotePropertyName 'TemplateList' -NotePropertyValue $Settings.TemplateIds -Force - } + return $true +} - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Template list after normalization: $($Settings.TemplateList | ConvertTo-Json -Compress)" -sev Debug +function Get-NormalizedTemplateList { + param($Settings) - if (-not $Settings.TemplateList) { - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to apply SafeLinks templates: No templates selected" -sev Error - return - } + if ($Settings.'standards.SafeLinksTemplatePolicy.TemplateIds') { + return $Settings.'standards.SafeLinksTemplatePolicy.TemplateIds' + } + elseif ($Settings.TemplateIds) { + return $Settings.TemplateIds + } - # Initialize overall results tracking - $OverallSuccess = $true - $TemplateResults = @{} + return $null +} - # Process each template - foreach ($Template in $Settings.TemplateList) { - $TemplateId = $Template.value - Write-Host "Working on template ID: $TemplateId" - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Processing SafeLinks template with ID: $TemplateId" -sev Info +function Get-SafeLinksTemplateFromStorage { + param($TemplateId) - # Get the template by GUID - $Table = Get-CippTable -tablename 'templates' - $Filter = "PartitionKey eq 'SafeLinksTemplate' and RowKey eq '$TemplateId'" - $Template = Get-CIPPAzDataTableEntity @Table -Filter $Filter + $Table = Get-CippTable -tablename 'templates' + $Filter = "PartitionKey eq 'SafeLinksTemplate' and RowKey eq '$TemplateId'" + $Template = Get-CIPPAzDataTableEntity @Table -Filter $Filter - if (-not $Template) { - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to apply SafeLinks template: Template with ID $TemplateId not found" -sev Error - $TemplateResults[$TemplateId] = @{ - Success = $false - Message = "Template with ID $TemplateId not found" - } - $OverallSuccess = $false - continue - } + if (-not $Template) { + throw "Template with ID $TemplateId not found" + } - # Parse the template JSON - try { - $PolicyConfig = $Template.JSON | ConvertFrom-Json -ErrorAction Stop - - # Helper function to process array fields - function Process-ArrayField { - param ( - [Parameter(Mandatory = $false)] - $Field - ) - - if ($null -eq $Field) { return @() } - - # If already an array, process each item - if ($Field -is [array]) { - $result = @() - foreach ($item in $Field) { - if ($item -is [string]) { - $result += $item - } - elseif ($item -is [hashtable] -or $item -is [PSCustomObject]) { - # Extract value from object - if ($null -ne $item.value) { - $result += $item.value - } - elseif ($null -ne $item.userPrincipalName) { - $result += $item.userPrincipalName - } - elseif ($null -ne $item.id) { - $result += $item.id - } - else { - $result += $item.ToString() - } - } - else { - $result += $item.ToString() - } - } - return $result - } - - # If it's a single object - if ($Field -is [hashtable] -or $Field -is [PSCustomObject]) { - if ($null -ne $Field.value) { return @($Field.value) } - if ($null -ne $Field.userPrincipalName) { return @($Field.userPrincipalName) } - if ($null -ne $Field.id) { return @($Field.id) } - } - - # If it's a string, return as an array with one item - if ($Field -is [string]) { - return @($Field) - } - - return @($Field) - } + return $Template.JSON | ConvertFrom-Json -ErrorAction Stop +} - # Extract policy name from template - $PolicyName = $PolicyConfig.PolicyName ?? $PolicyConfig.Name - $RuleName = $PolicyConfig.RuleName ?? $PolicyName - - # Process arrays in the template - $DoNotRewriteUrls = Process-ArrayField -Field $PolicyConfig.DoNotRewriteUrls - $SentTo = Process-ArrayField -Field $PolicyConfig.SentTo - $SentToMemberOf = Process-ArrayField -Field $PolicyConfig.SentToMemberOf - $RecipientDomainIs = Process-ArrayField -Field $PolicyConfig.RecipientDomainIs - $ExceptIfSentTo = Process-ArrayField -Field $PolicyConfig.ExceptIfSentTo - $ExceptIfSentToMemberOf = Process-ArrayField -Field $PolicyConfig.ExceptIfSentToMemberOf - $ExceptIfRecipientDomainIs = Process-ArrayField -Field $PolicyConfig.ExceptIfRecipientDomainIs - - # Check if policy and rule exist - $ExistingPoliciesParam = @{ - tenantid = $Tenant - cmdlet = 'Get-SafeLinksPolicy' - useSystemMailbox = $true - } +function ConvertTo-SafeArray { + param($Field) - $ExistingPolicies = New-ExoRequest @ExistingPoliciesParam - $PolicyExists = $ExistingPolicies | Where-Object { $_.Name -eq $PolicyName } + if ($null -eq $Field) { return @() } - $ExistingRulesParam = @{ - tenantid = $Tenant - cmdlet = 'Get-SafeLinksRule' - useSystemMailbox = $true - } + $ResultList = [System.Collections.Generic.List[string]]::new() - $ExistingRules = New-ExoRequest @ExistingRulesParam - $RuleExists = $ExistingRules | Where-Object { $_.Name -eq $RuleName } - - # Build policy parameters - $policyParams = @{} - - # Only add parameters that are explicitly provided in the template - if ($null -ne $PolicyConfig.EnableSafeLinksForEmail) { $policyParams.Add('EnableSafeLinksForEmail', $PolicyConfig.EnableSafeLinksForEmail) } - if ($null -ne $PolicyConfig.EnableSafeLinksForTeams) { $policyParams.Add('EnableSafeLinksForTeams', $PolicyConfig.EnableSafeLinksForTeams) } - if ($null -ne $PolicyConfig.EnableSafeLinksForOffice) { $policyParams.Add('EnableSafeLinksForOffice', $PolicyConfig.EnableSafeLinksForOffice) } - if ($null -ne $PolicyConfig.TrackClicks) { $policyParams.Add('TrackClicks', $PolicyConfig.TrackClicks) } - if ($null -ne $PolicyConfig.AllowClickThrough) { $policyParams.Add('AllowClickThrough', $PolicyConfig.AllowClickThrough) } - if ($null -ne $PolicyConfig.ScanUrls) { $policyParams.Add('ScanUrls', $PolicyConfig.ScanUrls) } - if ($null -ne $PolicyConfig.EnableForInternalSenders) { $policyParams.Add('EnableForInternalSenders', $PolicyConfig.EnableForInternalSenders) } - if ($null -ne $PolicyConfig.DeliverMessageAfterScan) { $policyParams.Add('DeliverMessageAfterScan', $PolicyConfig.DeliverMessageAfterScan) } - if ($null -ne $PolicyConfig.DisableUrlRewrite) { $policyParams.Add('DisableUrlRewrite', $PolicyConfig.DisableUrlRewrite) } - if ($null -ne $DoNotRewriteUrls -and $DoNotRewriteUrls.Count -gt 0) { $policyParams.Add('DoNotRewriteUrls', $DoNotRewriteUrls) } - if ($null -ne $PolicyConfig.AdminDisplayName) { $policyParams.Add('AdminDisplayName', $PolicyConfig.AdminDisplayName) } - if ($null -ne $PolicyConfig.CustomNotificationText) { $policyParams.Add('CustomNotificationText', $PolicyConfig.CustomNotificationText) } - if ($null -ne $PolicyConfig.EnableOrganizationBranding) { $policyParams.Add('EnableOrganizationBranding', $PolicyConfig.EnableOrganizationBranding) } - - # Build rule parameters - $ruleParams = @{} - - # Only add parameters that are explicitly provided - if ($null -ne $PolicyConfig.Priority) { $ruleParams.Add('Priority', $PolicyConfig.Priority) } - if ($null -ne $PolicyConfig.Description) { $ruleParams.Add('Comments', $PolicyConfig.Description) } - if ($null -ne $SentTo -and $SentTo.Count -gt 0) { $ruleParams.Add('SentTo', $SentTo) } - if ($null -ne $SentToMemberOf -and $SentToMemberOf.Count -gt 0) { $ruleParams.Add('SentToMemberOf', $SentToMemberOf) } - if ($null -ne $RecipientDomainIs -and $RecipientDomainIs.Count -gt 0) { $ruleParams.Add('RecipientDomainIs', $RecipientDomainIs) } - if ($null -ne $ExceptIfSentTo -and $ExceptIfSentTo.Count -gt 0) { $ruleParams.Add('ExceptIfSentTo', $ExceptIfSentTo) } - if ($null -ne $ExceptIfSentToMemberOf -and $ExceptIfSentToMemberOf.Count -gt 0) { $ruleParams.Add('ExceptIfSentToMemberOf', $ExceptIfSentToMemberOf) } - if ($null -ne $ExceptIfRecipientDomainIs -and $ExceptIfRecipientDomainIs.Count -gt 0) { $ruleParams.Add('ExceptIfRecipientDomainIs', $ExceptIfRecipientDomainIs) } - - $ActionsTaken = @() - - try { - if ($PolicyExists) { - # Update existing policy - $policyParams.Add('Identity', $PolicyName) - - $ExoPolicyRequestParam = @{ - tenantid = $Tenant - cmdlet = 'Set-SafeLinksPolicy' - cmdParams = $policyParams - useSystemMailbox = $true - } - - $null = New-ExoRequest @ExoPolicyRequestParam - $ActionsTaken += "Updated SafeLinks policy '$PolicyName'" - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Updated SafeLinks policy '$PolicyName'" -sev 'Info' - } - else { - # Create new policy - $policyParams.Add('Name', $PolicyName) - - $ExoPolicyRequestParam = @{ - tenantid = $Tenant - cmdlet = 'New-SafeLinksPolicy' - cmdParams = $policyParams - useSystemMailbox = $true - } - - $null = New-ExoRequest @ExoPolicyRequestParam - $ActionsTaken += "Created new SafeLinks policy '$PolicyName'" - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Created new SafeLinks policy '$PolicyName'" -sev 'Info' - } - - if ($RuleExists) { - # Update existing rule - $ruleParams.Add('Identity', $RuleName) - - $ExoRuleRequestParam = @{ - tenantid = $Tenant - cmdlet = 'Set-SafeLinksRule' - cmdParams = $ruleParams - useSystemMailbox = $true - } - - $null = New-ExoRequest @ExoRuleRequestParam - $ActionsTaken += "Updated SafeLinks rule '$RuleName'" - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Updated SafeLinks rule '$RuleName'" -sev 'Info' - } - else { - # Create new rule - $ruleParams.Add('Name', $RuleName) - $ruleParams.Add('SafeLinksPolicy', $PolicyName) - - $ExoRuleRequestParam = @{ - tenantid = $Tenant - cmdlet = 'New-SafeLinksRule' - cmdParams = $ruleParams - useSystemMailbox = $true - } - - $null = New-ExoRequest @ExoRuleRequestParam - $ActionsTaken += "Created new SafeLinks rule '$RuleName'" - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Created new SafeLinks rule '$RuleName'" -sev 'Info' - } - - # If State is specified in the template, enable or disable the rule - if ($null -ne $PolicyConfig.State) { - $Enabled = $PolicyConfig.State -eq "Enabled" - $EnableCmdlet = $Enabled ? 'Enable-SafeLinksRule' : 'Disable-SafeLinksRule' - $EnableRequestParam = @{ - tenantid = $Tenant - cmdlet = $EnableCmdlet - cmdParams = @{ - Identity = $RuleName - } - useSystemMailbox = $true - } - - $null = New-ExoRequest @EnableRequestParam - $StateMsg = $Enabled ? "enabled" : "disabled" - $ActionsTaken += "Rule $StateMsg" - Write-LogMessage -API 'Standards' -tenant $Tenant -message "SafeLinks rule '$RuleName' $StateMsg" -sev 'Info' - } - - $TemplateResults[$TemplateId] = @{ - Success = $true - ActionsTaken = $ActionsTaken - TemplateName = $PolicyConfig.Name - PolicyName = $PolicyName - RuleName = $RuleName - } - - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully applied SafeLinks template '$($PolicyConfig.Name)'" -sev 'Info' - } - catch { - $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - $TemplateResults[$TemplateId] = @{ - Success = $false - Message = $ErrorMessage - TemplateName = $PolicyConfig.Name - } - $OverallSuccess = $false - - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to apply SafeLinks template '$($PolicyConfig.Name)': $ErrorMessage" -sev 'Error' - } + if ($Field -is [array]) { + foreach ($item in $Field) { + if ($item -is [string]) { + $ResultList.Add($item) } - catch { - $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - $TemplateResults[$TemplateId] = @{ - Success = $false - Message = $ErrorMessage - } - $OverallSuccess = $false - - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to process template with ID $($TemplateId): $ErrorMessage" -sev 'Error' + elseif ($item.value) { + $ResultList.Add($item.value) + } + elseif ($item.userPrincipalName) { + $ResultList.Add($item.userPrincipalName) + } + elseif ($item.id) { + $ResultList.Add($item.id) + } + else { + $ResultList.Add($item.ToString()) } } + return $ResultList.ToArray() + } - # Report on overall results - if ($OverallSuccess) { - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully applied all SafeLinks templates" -sev 'Info' - } else { - $SuccessCount = ($TemplateResults.Values | Where-Object { $_.Success -eq $true }).Count - $TotalCount = $Settings.TemplateList.Count - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Applied $SuccessCount out of $TotalCount SafeLinks templates" -sev 'Info' + if ($Field -is [hashtable] -or $Field -is [PSCustomObject]) { + if ($Field.value) { + $ResultList.Add($Field.value) + return $ResultList.ToArray() + } + if ($Field.userPrincipalName) { + $ResultList.Add($Field.userPrincipalName) + return $ResultList.ToArray() } + if ($Field.id) { + $ResultList.Add($Field.id) + return $ResultList.ToArray() + } + } + + if ($Field -is [string]) { + $ResultList.Add($Field) + return $ResultList.ToArray() } - # Handle alert mode - if ($Settings.alert -eq $true) { - # Normalize the template list property based on what's passed - if ($Settings.'standards.SafeLinksTemplatePolicy.TemplateIds') { - $Settings | Add-Member -NotePropertyName 'TemplateList' -NotePropertyValue $Settings.'standards.SafeLinksTemplatePolicy.TemplateIds' -Force - } elseif ($Settings.TemplateIds) { - $Settings | Add-Member -NotePropertyName 'TemplateList' -NotePropertyValue $Settings.TemplateIds -Force + $ResultList.Add($Field.ToString()) + return $ResultList.ToArray() +} + +function Get-ExistingSafeLinksObjects { + param($Tenant, $PolicyName, $RuleName) + + $PolicyExists = $null + $RuleExists = $null + + try { + $ExistingPolicies = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-SafeLinksPolicy' -useSystemMailbox $true + $PolicyExists = $ExistingPolicies | Where-Object { $_.Name -eq $PolicyName } + } + catch { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to retrieve existing policies: $($_.Exception.Message)" -sev Warning + } + + try { + $ExistingRules = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-SafeLinksRule' -useSystemMailbox $true + $RuleExists = $ExistingRules | Where-Object { $_.Name -eq $RuleName } + } + catch { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to retrieve existing rules: $($_.Exception.Message)" -sev Warning + } + + return @{ + PolicyExists = $PolicyExists + RuleExists = $RuleExists + } +} + +function New-SafeLinksPolicyParameters { + param($Template) + + $PolicyMappings = @{ + 'EnableSafeLinksForEmail' = 'EnableSafeLinksForEmail' + 'EnableSafeLinksForTeams' = 'EnableSafeLinksForTeams' + 'EnableSafeLinksForOffice' = 'EnableSafeLinksForOffice' + 'TrackClicks' = 'TrackClicks' + 'AllowClickThrough' = 'AllowClickThrough' + 'ScanUrls' = 'ScanUrls' + 'EnableForInternalSenders' = 'EnableForInternalSenders' + 'DeliverMessageAfterScan' = 'DeliverMessageAfterScan' + 'DisableUrlRewrite' = 'DisableUrlRewrite' + 'AdminDisplayName' = 'AdminDisplayName' + 'CustomNotificationText' = 'CustomNotificationText' + 'EnableOrganizationBranding' = 'EnableOrganizationBranding' + } + + $PolicyParams = @{} + + foreach ($templateKey in $PolicyMappings.Keys) { + if ($null -ne $Template.$templateKey) { + $PolicyParams[$PolicyMappings[$templateKey]] = $Template.$templateKey } + } + + $DoNotRewriteUrls = ConvertTo-SafeArray -Field $Template.DoNotRewriteUrls + if ($DoNotRewriteUrls.Count -gt 0) { + $PolicyParams['DoNotRewriteUrls'] = $DoNotRewriteUrls + } + + return $PolicyParams +} + +function New-SafeLinksRuleParameters { + param($Template) + + $RuleParams = @{} + + # Basic rule parameters + if ($null -ne $Template.Priority) { $RuleParams['Priority'] = $Template.Priority } + if ($null -ne $Template.Description) { $RuleParams['Comments'] = $Template.Description } + if ($null -ne $Template.TemplateDescription) { $RuleParams['Comments'] = $Template.TemplateDescription } - if (-not $Settings.TemplateList) { - Write-StandardsAlert -message "SafeLinks templates could not be checked: No templates selected" -object $null -tenant $Tenant -standardName 'SafeLinksTemplatePolicy' -standardId $Settings.standardId - Write-LogMessage -API 'Standards' -tenant $Tenant -message "SafeLinks templates could not be checked: No templates selected" -sev Info - return + # Array-based rule parameters + $ArrayMappings = @{ + 'SentTo' = ConvertTo-SafeArray -Field $Template.SentTo + 'SentToMemberOf' = ConvertTo-SafeArray -Field $Template.SentToMemberOf + 'RecipientDomainIs' = ConvertTo-SafeArray -Field $Template.RecipientDomainIs + 'ExceptIfSentTo' = ConvertTo-SafeArray -Field $Template.ExceptIfSentTo + 'ExceptIfSentToMemberOf' = ConvertTo-SafeArray -Field $Template.ExceptIfSentToMemberOf + 'ExceptIfRecipientDomainIs' = ConvertTo-SafeArray -Field $Template.ExceptIfRecipientDomainIs + } + + foreach ($paramName in $ArrayMappings.Keys) { + if ($ArrayMappings[$paramName].Count -gt 0) { + $RuleParams[$paramName] = $ArrayMappings[$paramName] } + } - $AllTemplatesApplied = $true - $AlertMessages = @() + return $RuleParams +} - foreach ($Template in $Settings.TemplateList) { - $TemplateId = $Template.value +function Set-SafeLinksRuleState { + param($Tenant, $RuleName, $State) - # Get the template - $Table = Get-CippTable -tablename 'templates' - $Filter = "PartitionKey eq 'SafeLinksTemplate' and RowKey eq '$TemplateId'" - $Template = Get-CIPPAzDataTableEntity @Table -Filter $Filter + if ($null -eq $State) { return } - if (-not $Template) { - $AlertMessages += "Template with ID $TemplateId not found" - $AllTemplatesApplied = $false - continue - } + $IsEnabled = switch ($State) { + "Enabled" { $true } + "Disabled" { $false } + $true { $true } + $false { $false } + default { $null } + } - try { - $PolicyConfig = $Template.JSON | ConvertFrom-Json -ErrorAction Stop - $PolicyName = $PolicyConfig.PolicyName ?? $PolicyConfig.Name - $RuleName = $PolicyConfig.RuleName ?? $PolicyName + if ($null -ne $IsEnabled) { + $Cmdlet = $IsEnabled ? 'Enable-SafeLinksRule' : 'Disable-SafeLinksRule' + $null = New-ExoRequest -tenantid $Tenant -cmdlet $Cmdlet -cmdParams @{ Identity = $RuleName } -useSystemMailbox $true + return $IsEnabled ? "enabled" : "disabled" + } - # Check if policy and rule exist - $ExistingPoliciesParam = @{ - tenantid = $Tenant - cmdlet = 'Get-SafeLinksPolicy' - useSystemMailbox = $true - } + return $null +} - $ExistingPolicies = New-ExoRequest @ExistingPoliciesParam - $PolicyExists = $ExistingPolicies | Where-Object { $_.Name -eq $PolicyName } +function Invoke-SafeLinksRemediation { + param($Tenant, $TemplateList, $Settings) - $ExistingRulesParam = @{ - tenantid = $Tenant - cmdlet = 'Get-SafeLinksRule' - useSystemMailbox = $true - } + $OverallSuccess = $true + $TemplateResults = @{} - $ExistingRules = New-ExoRequest @ExistingRulesParam - $RuleExists = $ExistingRules | Where-Object { $_.Name -eq $RuleName } + foreach ($TemplateItem in $TemplateList) { + $TemplateId = $TemplateItem.value - if (-not $PolicyExists -or -not $RuleExists) { - $AllTemplatesApplied = $false - $Status = "SafeLinks template '$($PolicyConfig.Name)' is not applied" + try { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Processing SafeLinks template with ID: $TemplateId" -sev Info - if (-not $PolicyExists) { - $Status += " - policy '$PolicyName' does not exist" - } + # Get template from storage + $Template = Get-SafeLinksTemplateFromStorage -TemplateId $TemplateId - if (-not $RuleExists) { - $Status += " - rule '$RuleName' does not exist" - } + $PolicyName = $Template.PolicyName ?? $Template.Name + $RuleName = $Template.RuleName ?? "$($PolicyName)_Rule" - $AlertMessages += $Status - } + # Check existing objects + $ExistingObjects = Get-ExistingSafeLinksObjects -Tenant $Tenant -PolicyName $PolicyName -RuleName $RuleName + + $ActionsTaken = [System.Collections.Generic.List[string]]::new() + + # Process Policy + $PolicyParams = New-SafeLinksPolicyParameters -Template $Template + + if ($ExistingObjects.PolicyExists) { + # Update existing policy to keep it in line + $PolicyParams['Identity'] = $PolicyName + $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-SafeLinksPolicy' -cmdParams $PolicyParams -useSystemMailbox $true + $ActionsTaken.Add("Updated SafeLinks policy '$PolicyName'") + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Updated SafeLinks policy '$PolicyName'" -sev Info } - catch { - $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - $AlertMessages += "Failed to check template with ID $($TemplateId): $ErrorMessage" - $AllTemplatesApplied = $false + else { + # Create new policy + $PolicyParams['Name'] = $PolicyName + $null = New-ExoRequest -tenantid $Tenant -cmdlet 'New-SafeLinksPolicy' -cmdParams $PolicyParams -useSystemMailbox $true + $ActionsTaken.Add("Created new SafeLinks policy '$PolicyName'") + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Created new SafeLinks policy '$PolicyName'" -sev Info } - } - if ($AllTemplatesApplied) { - Write-LogMessage -API 'Standards' -tenant $Tenant -message "All SafeLinks templates are correctly applied" -sev 'Info' + # Process Rule + $RuleParams = New-SafeLinksRuleParameters -Template $Template + + if ($ExistingObjects.RuleExists) { + # Update existing rule to keep it in line + $RuleParams['Identity'] = $RuleName + $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-SafeLinksRule' -cmdParams $RuleParams -useSystemMailbox $true + $ActionsTaken.Add("Updated SafeLinks rule '$RuleName'") + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Updated SafeLinks rule '$RuleName'" -sev Info + } + else { + # Create new rule + $RuleParams['Name'] = $RuleName + $RuleParams['SafeLinksPolicy'] = $PolicyName + $null = New-ExoRequest -tenantid $Tenant -cmdlet 'New-SafeLinksRule' -cmdParams $RuleParams -useSystemMailbox $true + $ActionsTaken.Add("Created new SafeLinks rule '$RuleName'") + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Created new SafeLinks rule '$RuleName'" -sev Info + } + + # Set rule state + $StateResult = Set-SafeLinksRuleState -Tenant $Tenant -RuleName $RuleName -State $Template.State + if ($StateResult) { + $ActionsTaken.Add("Rule $StateResult") + Write-LogMessage -API 'Standards' -tenant $Tenant -message "SafeLinks rule '$RuleName' $StateResult" -sev Info + } + + $TemplateResults[$TemplateId] = @{ + Success = $true + ActionsTaken = $ActionsTaken.ToArray() + TemplateName = $Template.TemplateName ?? $Template.Name + PolicyName = $PolicyName + RuleName = $RuleName + } + + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully applied SafeLinks template '$($Template.TemplateName ?? $Template.Name)'" -sev Info } - else { - $AlertMessage = "One or more SafeLinks templates are not correctly applied: " + ($AlertMessages -join " | ") - Write-StandardsAlert -message $AlertMessage -object @{ - Templates = $Settings.TemplateList - Issues = $AlertMessages - } -tenant $Tenant -standardName 'SafeLinksTemplatePolicy' -standardId $Settings.standardId - - Write-LogMessage -API 'Standards' -tenant $Tenant -message $AlertMessage -sev 'Info' + catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + $TemplateResults[$TemplateId] = @{ + Success = $false + Message = $ErrorMessage + TemplateName = $Template.TemplateName ?? $Template.Name ?? "Unknown" + } + $OverallSuccess = $false + + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to apply SafeLinks template ID $TemplateId : $ErrorMessage" -sev Error } } - # Handle report mode - if ($Settings.report -eq $true) { - # Normalize the template list property based on what's passed - if ($Settings.'standards.SafeLinksTemplatePolicy.TemplateIds') { - $Settings | Add-Member -NotePropertyName 'TemplateList' -NotePropertyValue $Settings.'standards.SafeLinksTemplatePolicy.TemplateIds' -Force - } elseif ($Settings.TemplateIds) { - $Settings | Add-Member -NotePropertyName 'TemplateList' -NotePropertyValue $Settings.TemplateIds -Force - } + # Report overall results + if ($OverallSuccess) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully applied all SafeLinks templates" -sev Info + } + else { + $SuccessCount = ($TemplateResults.Values | Where-Object { $_.Success -eq $true }).Count + $TotalCount = $TemplateList.Count + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Applied $SuccessCount out of $TotalCount SafeLinks templates" -sev Info + } +} - if (-not $Settings.TemplateList) { - Add-CIPPBPAField -FieldName 'SafeLinksTemplatePolicy' -FieldValue $false -StoreAs bool -Tenant $tenant - Set-CIPPStandardsCompareField -FieldName 'standards.SafeLinksTemplatePolicy' -FieldValue "No templates selected" -Tenant $Tenant - return - } +function Invoke-SafeLinksAlert { + param($Tenant, $TemplateList, $Settings) - $AllTemplatesApplied = $true - $ReportResults = @{} + $AllTemplatesApplied = $true + $AlertMessages = [System.Collections.Generic.List[string]]::new() - foreach ($Template in $Settings.TemplateList) { - $TemplateId = $Template.value + foreach ($TemplateItem in $TemplateList) { + $TemplateId = $TemplateItem.value - # Get the template - $Table = Get-CippTable -tablename 'templates' - $Filter = "PartitionKey eq 'SafeLinksTemplate' and RowKey eq '$TemplateId'" - $Template = Get-CIPPAzDataTableEntity @Table -Filter $Filter + try { + $Template = Get-SafeLinksTemplateFromStorage -TemplateId $TemplateId + $PolicyName = $Template.PolicyName ?? $Template.Name + $RuleName = $Template.RuleName ?? "$($PolicyName)_Rule" - if (-not $Template) { - $ReportResults[$TemplateId] = @{ - Success = $false - Message = "Template not found" - } + $ExistingObjects = Get-ExistingSafeLinksObjects -Tenant $Tenant -PolicyName $PolicyName -RuleName $RuleName + + if (-not $ExistingObjects.PolicyExists -or -not $ExistingObjects.RuleExists) { $AllTemplatesApplied = $false - continue - } + $Status = "SafeLinks template '$($Template.TemplateName ?? $Template.Name)' is not applied" - try { - $PolicyConfig = $Template.JSON | ConvertFrom-Json -ErrorAction Stop - $PolicyName = $PolicyConfig.PolicyName ?? $PolicyConfig.Name - $RuleName = $PolicyConfig.RuleName ?? $PolicyName + if (-not $ExistingObjects.PolicyExists) { + $Status = "$Status - policy '$PolicyName' does not exist" + } - # Check if policy and rule exist - $ExistingPoliciesParam = @{ - tenantid = $Tenant - cmdlet = 'Get-SafeLinksPolicy' - useSystemMailbox = $true + if (-not $ExistingObjects.RuleExists) { + $Status = "$Status - rule '$RuleName' does not exist" } - $ExistingPolicies = New-ExoRequest @ExistingPoliciesParam - $PolicyExists = $ExistingPolicies | Where-Object { $_.Name -eq $PolicyName } + $AlertMessages.Add($Status) + } + } + catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + $AlertMessages.Add("Failed to check template with ID $TemplateId : $ErrorMessage") + $AllTemplatesApplied = $false + } + } - $ExistingRulesParam = @{ - tenantid = $Tenant - cmdlet = 'Get-SafeLinksRule' - useSystemMailbox = $true - } + if ($AllTemplatesApplied) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "All SafeLinks templates are correctly applied" -sev Info + } + else { + $AlertMessage = "One or more SafeLinks templates are not correctly applied: " + ($AlertMessages.ToArray() -join " | ") + Write-StandardsAlert -message $AlertMessage -object @{ + Templates = $TemplateList + Issues = $AlertMessages.ToArray() + } -tenant $Tenant -standardName 'SafeLinksTemplatePolicy' -standardId $Settings.standardId + + Write-LogMessage -API 'Standards' -tenant $Tenant -message $AlertMessage -sev Info + } +} - $ExistingRules = New-ExoRequest @ExistingRulesParam - $RuleExists = $ExistingRules | Where-Object { $_.Name -eq $RuleName } +function Invoke-SafeLinksReport { + param($Tenant, $TemplateList, $Settings) - $ReportResults[$TemplateId] = @{ - Success = ($PolicyExists -and $RuleExists) - TemplateName = $PolicyConfig.Name - PolicyName = $PolicyName - RuleName = $RuleName - PolicyExists = $PolicyExists - RuleExists = $RuleExists - } + $AllTemplatesApplied = $true + $ReportResults = @{} - if (-not $PolicyExists -or -not $RuleExists) { - $AllTemplatesApplied = $false - } + foreach ($TemplateItem in $TemplateList) { + $TemplateId = $TemplateItem.value + + try { + $Template = Get-SafeLinksTemplateFromStorage -TemplateId $TemplateId + $PolicyName = $Template.PolicyName ?? $Template.Name + $RuleName = $Template.RuleName ?? "$($PolicyName)_Rule" + + $ExistingObjects = Get-ExistingSafeLinksObjects -Tenant $Tenant -PolicyName $PolicyName -RuleName $RuleName + + $ReportResults[$TemplateId] = @{ + Success = ($ExistingObjects.PolicyExists -and $ExistingObjects.RuleExists) + TemplateName = $Template.TemplateName ?? $Template.Name + PolicyName = $PolicyName + RuleName = $RuleName + PolicyExists = [bool]$ExistingObjects.PolicyExists + RuleExists = [bool]$ExistingObjects.RuleExists } - catch { - $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - $ReportResults[$TemplateId] = @{ - Success = $false - Message = $ErrorMessage - } + + if (-not $ExistingObjects.PolicyExists -or -not $ExistingObjects.RuleExists) { $AllTemplatesApplied = $false } } + catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + $ReportResults[$TemplateId] = @{ + Success = $false + Message = $ErrorMessage + } + $AllTemplatesApplied = $false + } + } - Add-CIPPBPAField -FieldName 'SafeLinksTemplatePolicy' -FieldValue $AllTemplatesApplied -StoreAs bool -Tenant $tenant + Add-CIPPBPAField -FieldName 'SafeLinksTemplatePolicy' -FieldValue $AllTemplatesApplied -StoreAs bool -Tenant $Tenant - if ($AllTemplatesApplied) { - Set-CIPPStandardsCompareField -FieldName 'standards.SafeLinksTemplatePolicy' -FieldValue $true -Tenant $Tenant - } - else { - Set-CIPPStandardsCompareField -FieldName 'standards.SafeLinksTemplatePolicy' -FieldValue @{ - TemplateResults = $ReportResults - ProcessedTemplates = $Settings.TemplateList.Count - SuccessfulTemplates = ($ReportResults.Values | Where-Object { $_.Success -eq $true }).Count - } -Tenant $Tenant - } + if ($AllTemplatesApplied) { + Set-CIPPStandardsCompareField -FieldName 'standards.SafeLinksTemplatePolicy' -FieldValue $true -Tenant $Tenant + } + else { + Set-CIPPStandardsCompareField -FieldName 'standards.SafeLinksTemplatePolicy' -FieldValue @{ + TemplateResults = $ReportResults + ProcessedTemplates = $TemplateList.Count + SuccessfulTemplates = ($ReportResults.Values | Where-Object { $_.Success -eq $true }).Count + } -Tenant $Tenant } } From b501faebf41d06b052bbf8281c95e76f20eab83b Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 3 Jun 2025 09:13:24 +0800 Subject: [PATCH 024/160] State Fixes --- .../Invoke-AddSafeLinksPolicyFromTemplate.ps1 | 10 +++++----- .../Safe-Links-Policy/Invoke-EditSafeLinksPolicy.ps1 | 6 +++--- .../Invoke-ExecNewSafeLinksPolicy.ps1 | 2 +- .../Safe-Links-Policy/Invoke-ListSafeLinksPolicy.ps1 | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyFromTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyFromTemplate.ps1 index 5512afc6544a..b9fb6292bf32 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyFromTemplate.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyFromTemplate.ps1 @@ -175,9 +175,9 @@ Function Invoke-AddSafeLinksPolicyFromTemplate { Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Created SafeLinks rule '$RuleName'" -Sev 'Info' # Handle rule state - $StateMessage = "" - if ($null -ne $Template.State) { - $IsEnabled = switch ($Template.State) { + $EnabledMessage = "" + if ($null -ne $Template.Enabled) { + $IsEnabled = switch ($Template.Enabled) { "Enabled" { $true } "Disabled" { $false } $true { $true } @@ -188,11 +188,11 @@ Function Invoke-AddSafeLinksPolicyFromTemplate { if ($null -ne $IsEnabled) { $Cmdlet = $IsEnabled ? 'Enable-SafeLinksRule' : 'Disable-SafeLinksRule' $null = New-ExoRequest -tenantid $TenantFilter -cmdlet $Cmdlet -cmdParams @{ Identity = $RuleName } -useSystemMailbox $true - $StateMessage = " (rule $($IsEnabled ? 'enabled' : 'disabled'))" + $EnabledMessage = " (rule $($IsEnabled ? 'enabled' : 'disabled'))" } } - return "Successfully deployed SafeLinks policy '$PolicyName' and rule '$RuleName' to tenant $TenantFilter$StateMessage" + return "Successfully deployed SafeLinks policy '$PolicyName' and rule '$RuleName' to tenant $TenantFilter$EnabledMessage" } # Process each tenant and template combination diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-EditSafeLinksPolicy.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-EditSafeLinksPolicy.ps1 index 2e70ec922fb6..7e1d6a623cb3 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-EditSafeLinksPolicy.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-EditSafeLinksPolicy.ps1 @@ -92,7 +92,7 @@ function Invoke-EditSafeLinksPolicy { # Extract rule parameters from body $Priority = $Request.Body.Priority $Comments = $Request.Body.Comments - $Enabled = $Request.Body.State + $Enabled = $Request.Body.Enabled # Process recipient-related parameters $SentTo = Process-ArrayField -Field $Request.Body.SentTo @@ -189,8 +189,8 @@ function Invoke-EditSafeLinksPolicy { $null = New-ExoRequest @EnableRequestParam $hasRuleOperation = $true - $state = $Enabled ? "enabled" : "disabled" - $ruleMessages.Add($state) + $Enabled = $Enabled ? "enabled" : "disabled" + $ruleMessages.Add($Enabled) } # Add combined rule message if any rule operations were performed diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecNewSafeLinksPolicy.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecNewSafeLinksPolicy.ps1 index 94db830cba75..844260ad5b0b 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecNewSafeLinksPolicy.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecNewSafeLinksPolicy.ps1 @@ -38,7 +38,7 @@ function Invoke-ExecNewSafeLinksPolicy { # Extract rule settings from body $Priority = $Request.Body.Priority $Comments = $Request.Body.Comments - $Enabled = $Request.Body.State + $Enabled = $Request.Body.Enabled $RuleName = $Request.Body.RuleName # Extract recipient fields and handle different input formats diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicy.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicy.ps1 index a3a347e6526e..006320f9a6a8 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicy.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicy.ps1 @@ -73,7 +73,7 @@ Function Invoke-ListSafeLinksPolicy { PolicyName = $policyName RuleName = $associatedRule.Name Priority = if ($matchingBuiltInRule) { $matchingBuiltInRule.Priority } else { $associatedRule.Priority } - State = if ($matchingBuiltInRule) { $matchingBuiltInRule.State } else { $associatedRule.State } + State = if ($matchingBuiltInRule) { $matchingBuiltInRule.Enabled } else { $associatedRule.Enabled } SentTo = $associatedRule.SentTo SentToMemberOf = $associatedRule.SentToMemberOf RecipientDomainIs = $associatedRule.RecipientDomainIs @@ -112,7 +112,7 @@ Function Invoke-ListSafeLinksPolicy { PolicyName = $rule.SafeLinksPolicy RuleName = $rule.Name Priority = $rule.Priority - State = $rule.State + State = $rule.Enabled SentTo = $rule.SentTo SentToMemberOf = $rule.SentToMemberOf RecipientDomainIs = $rule.RecipientDomainIs @@ -153,7 +153,7 @@ Function Invoke-ListSafeLinksPolicy { PolicyName = $null RuleName = $builtInRule.Name Priority = $builtInRule.Priority - State = $builtInRule.State + State = $builtInRule.Enabled SentTo = $builtInRule.SentTo SentToMemberOf = $builtInRule.SentToMemberOf RecipientDomainIs = $builtInRule.RecipientDomainIs From 94852b0eb673fb92495db6061aac048e3936b871 Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 3 Jun 2025 09:17:37 +0800 Subject: [PATCH 025/160] Enabled fixes --- .../Safe-Links-Policy/Invoke-AddSafeLinksPolicyTemplate.ps1 | 2 +- .../Invoke-CreateSafeLinksPolicyTemplate.ps1 | 2 +- .../Invoke-EditSafeLinksPolicyTemplate.ps1 | 2 +- .../Safe-Links-Policy/Invoke-ListSafeLinksPolicy.ps1 | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyTemplate.ps1 index 75ef6af27675..673431e80e64 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyTemplate.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyTemplate.ps1 @@ -51,7 +51,7 @@ Function Invoke-AddSafeLinksPolicyTemplate { "DeliverMessageAfterScan", "DisableUrlRewrite", "DoNotRewriteUrls", "AdminDisplayName", "CustomNotificationText", "EnableOrganizationBranding", # Rule properties - "RuleName", "Priority", "State", "Comments", + "RuleName", "Priority", "Enabled", "Comments", "SentTo", "SentToMemberOf", "RecipientDomainIs", "ExceptIfSentTo", "ExceptIfSentToMemberOf", "ExceptIfRecipientDomainIs" ) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-CreateSafeLinksPolicyTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-CreateSafeLinksPolicyTemplate.ps1 index 9689856a1274..ade782e0b549 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-CreateSafeLinksPolicyTemplate.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-CreateSafeLinksPolicyTemplate.ps1 @@ -35,7 +35,7 @@ Function Invoke-CreateSafeLinksPolicyTemplate { "AdminDisplayName", "CustomNotificationText", "EnableOrganizationBranding", # Rule properties - "RuleName", "Priority", "State", "Comments", + "RuleName", "Priority", "Enabled", "Comments", "SentTo", "SentToMemberOf", "RecipientDomainIs", "ExceptIfSentTo", "ExceptIfSentToMemberOf", "ExceptIfRecipientDomainIs" ) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-EditSafeLinksPolicyTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-EditSafeLinksPolicyTemplate.ps1 index dc9bbf0fad5b..f4e01b9461e6 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-EditSafeLinksPolicyTemplate.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-EditSafeLinksPolicyTemplate.ps1 @@ -48,7 +48,7 @@ Function Invoke-EditSafeLinksPolicyTemplate { "AdminDisplayName", "CustomNotificationText", "EnableOrganizationBranding", # Rule properties - "RuleName", "Priority", "State", "Comments", + "RuleName", "Priority", "Enabled", "Comments", "SentTo", "SentToMemberOf", "RecipientDomainIs", "ExceptIfSentTo", "ExceptIfSentToMemberOf", "ExceptIfRecipientDomainIs" ) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicy.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicy.ps1 index 006320f9a6a8..9bfdff2d690d 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicy.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicy.ps1 @@ -73,7 +73,7 @@ Function Invoke-ListSafeLinksPolicy { PolicyName = $policyName RuleName = $associatedRule.Name Priority = if ($matchingBuiltInRule) { $matchingBuiltInRule.Priority } else { $associatedRule.Priority } - State = if ($matchingBuiltInRule) { $matchingBuiltInRule.Enabled } else { $associatedRule.Enabled } + Enabled = if ($matchingBuiltInRule) { $matchingBuiltInRule.Enabled } else { $associatedRule.Enabled } SentTo = $associatedRule.SentTo SentToMemberOf = $associatedRule.SentToMemberOf RecipientDomainIs = $associatedRule.RecipientDomainIs @@ -112,7 +112,7 @@ Function Invoke-ListSafeLinksPolicy { PolicyName = $rule.SafeLinksPolicy RuleName = $rule.Name Priority = $rule.Priority - State = $rule.Enabled + Enabled = $rule.Enabled SentTo = $rule.SentTo SentToMemberOf = $rule.SentToMemberOf RecipientDomainIs = $rule.RecipientDomainIs @@ -153,7 +153,7 @@ Function Invoke-ListSafeLinksPolicy { PolicyName = $null RuleName = $builtInRule.Name Priority = $builtInRule.Priority - State = $builtInRule.Enabled + Enabled = $builtInRule.Enabled SentTo = $builtInRule.SentTo SentToMemberOf = $builtInRule.SentToMemberOf RecipientDomainIs = $builtInRule.RecipientDomainIs From 9dce385edb1fecf36ea39bdbd6ad3e46900784e2 Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 3 Jun 2025 09:31:51 +0800 Subject: [PATCH 026/160] Revert to State from Enabled --- .../Invoke-AddSafeLinksPolicyFromTemplate.ps1 | 14 +++++++------- .../Invoke-AddSafeLinksPolicyTemplate.ps1 | 2 +- .../Invoke-CreateSafeLinksPolicyTemplate.ps1 | 2 +- .../Invoke-EditSafeLinksPolicy.ps1 | 10 +++++----- .../Invoke-EditSafeLinksPolicyTemplate.ps1 | 2 +- .../Invoke-ExecNewSafeLinksPolicy.ps1 | 8 ++++---- .../Invoke-ListSafeLinksPolicy.ps1 | 7 ++++--- 7 files changed, 23 insertions(+), 22 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyFromTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyFromTemplate.ps1 index b9fb6292bf32..fe506d605367 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyFromTemplate.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyFromTemplate.ps1 @@ -175,9 +175,9 @@ Function Invoke-AddSafeLinksPolicyFromTemplate { Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Created SafeLinks rule '$RuleName'" -Sev 'Info' # Handle rule state - $EnabledMessage = "" - if ($null -ne $Template.Enabled) { - $IsEnabled = switch ($Template.Enabled) { + $StateMessage = "" + if ($null -ne $Template.State) { + $IsState = switch ($Template.State) { "Enabled" { $true } "Disabled" { $false } $true { $true } @@ -185,14 +185,14 @@ Function Invoke-AddSafeLinksPolicyFromTemplate { default { $null } } - if ($null -ne $IsEnabled) { - $Cmdlet = $IsEnabled ? 'Enable-SafeLinksRule' : 'Disable-SafeLinksRule' + if ($null -ne $IsState) { + $Cmdlet = $IsState ? 'Enable-SafeLinksRule' : 'Disable-SafeLinksRule' $null = New-ExoRequest -tenantid $TenantFilter -cmdlet $Cmdlet -cmdParams @{ Identity = $RuleName } -useSystemMailbox $true - $EnabledMessage = " (rule $($IsEnabled ? 'enabled' : 'disabled'))" + $StateMessage = " (rule $($IsState ? 'enabled' : 'disabled'))" } } - return "Successfully deployed SafeLinks policy '$PolicyName' and rule '$RuleName' to tenant $TenantFilter$EnabledMessage" + return "Successfully deployed SafeLinks policy '$PolicyName' and rule '$RuleName' to tenant $TenantFilter$StateMessage" } # Process each tenant and template combination diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyTemplate.ps1 index 673431e80e64..75ef6af27675 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyTemplate.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyTemplate.ps1 @@ -51,7 +51,7 @@ Function Invoke-AddSafeLinksPolicyTemplate { "DeliverMessageAfterScan", "DisableUrlRewrite", "DoNotRewriteUrls", "AdminDisplayName", "CustomNotificationText", "EnableOrganizationBranding", # Rule properties - "RuleName", "Priority", "Enabled", "Comments", + "RuleName", "Priority", "State", "Comments", "SentTo", "SentToMemberOf", "RecipientDomainIs", "ExceptIfSentTo", "ExceptIfSentToMemberOf", "ExceptIfRecipientDomainIs" ) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-CreateSafeLinksPolicyTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-CreateSafeLinksPolicyTemplate.ps1 index ade782e0b549..9689856a1274 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-CreateSafeLinksPolicyTemplate.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-CreateSafeLinksPolicyTemplate.ps1 @@ -35,7 +35,7 @@ Function Invoke-CreateSafeLinksPolicyTemplate { "AdminDisplayName", "CustomNotificationText", "EnableOrganizationBranding", # Rule properties - "RuleName", "Priority", "Enabled", "Comments", + "RuleName", "Priority", "State", "Comments", "SentTo", "SentToMemberOf", "RecipientDomainIs", "ExceptIfSentTo", "ExceptIfSentToMemberOf", "ExceptIfRecipientDomainIs" ) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-EditSafeLinksPolicy.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-EditSafeLinksPolicy.ps1 index 7e1d6a623cb3..118b7c22b8c8 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-EditSafeLinksPolicy.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-EditSafeLinksPolicy.ps1 @@ -92,7 +92,7 @@ function Invoke-EditSafeLinksPolicy { # Extract rule parameters from body $Priority = $Request.Body.Priority $Comments = $Request.Body.Comments - $Enabled = $Request.Body.Enabled + $State = $Request.Body.State # Process recipient-related parameters $SentTo = Process-ArrayField -Field $Request.Body.SentTo @@ -176,8 +176,8 @@ function Invoke-EditSafeLinksPolicy { } # Handle enable/disable if needed - if ($null -ne $Enabled) { - $EnableCmdlet = $Enabled ? 'Enable-SafeLinksRule' : 'Disable-SafeLinksRule' + if ($null -ne $State) { + $EnableCmdlet = $State ? 'Enable-SafeLinksRule' : 'Disable-SafeLinksRule' $EnableRequestParam = @{ tenantid = $TenantFilter cmdlet = $EnableCmdlet @@ -189,8 +189,8 @@ function Invoke-EditSafeLinksPolicy { $null = New-ExoRequest @EnableRequestParam $hasRuleOperation = $true - $Enabled = $Enabled ? "enabled" : "disabled" - $ruleMessages.Add($Enabled) + $State = $State ? "enabled" : "disabled" + $ruleMessages.Add($State) } # Add combined rule message if any rule operations were performed diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-EditSafeLinksPolicyTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-EditSafeLinksPolicyTemplate.ps1 index f4e01b9461e6..dc9bbf0fad5b 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-EditSafeLinksPolicyTemplate.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-EditSafeLinksPolicyTemplate.ps1 @@ -48,7 +48,7 @@ Function Invoke-EditSafeLinksPolicyTemplate { "AdminDisplayName", "CustomNotificationText", "EnableOrganizationBranding", # Rule properties - "RuleName", "Priority", "Enabled", "Comments", + "RuleName", "Priority", "State", "Comments", "SentTo", "SentToMemberOf", "RecipientDomainIs", "ExceptIfSentTo", "ExceptIfSentToMemberOf", "ExceptIfRecipientDomainIs" ) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecNewSafeLinksPolicy.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecNewSafeLinksPolicy.ps1 index 844260ad5b0b..8cc0c5c34cf3 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecNewSafeLinksPolicy.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecNewSafeLinksPolicy.ps1 @@ -38,7 +38,7 @@ function Invoke-ExecNewSafeLinksPolicy { # Extract rule settings from body $Priority = $Request.Body.Priority $Comments = $Request.Body.Comments - $Enabled = $Request.Body.Enabled + $State = $Request.Body.State $RuleName = $Request.Body.RuleName # Extract recipient fields and handle different input formats @@ -192,9 +192,9 @@ function Invoke-ExecNewSafeLinksPolicy { $null = New-ExoRequest @ExoRuleRequestParam - # If Enabled is specified, enable or disable the rule - if ($null -ne $Enabled) { - $EnableCmdlet = $Enabled ? 'Enable-SafeLinksRule' : 'Disable-SafeLinksRule' + # If State is specified, enable or disable the rule + if ($null -ne $State) { + $EnableCmdlet = $State ? 'Enable-SafeLinksRule' : 'Disable-SafeLinksRule' $EnableRequestParam = @{ tenantid = $TenantFilter cmdlet = $EnableCmdlet diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicy.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicy.ps1 index 9bfdff2d690d..22c07bb966d9 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicy.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicy.ps1 @@ -19,6 +19,7 @@ Function Invoke-ListSafeLinksPolicy { $Policies = New-ExoRequest -tenantid $Tenantfilter -cmdlet 'Get-SafeLinksPolicy' | Select-Object -Property * -ExcludeProperty '*@odata.type' , '*@data.type' $Rules = New-ExoRequest -tenantid $Tenantfilter -cmdlet 'Get-SafeLinksRule' | Select-Object -Property * -ExcludeProperty '*@odata.type' , '*@data.type' $BuiltInRules = New-ExoRequest -tenantid $Tenantfilter -cmdlet 'Get-EOPProtectionPolicyRule' | Select-Object -Property * -ExcludeProperty '*@odata.type' , '*@data.type' + write-host $Rules # Track matched items to identify orphans $MatchedRules = [System.Collections.Generic.HashSet[string]]::new() @@ -73,7 +74,7 @@ Function Invoke-ListSafeLinksPolicy { PolicyName = $policyName RuleName = $associatedRule.Name Priority = if ($matchingBuiltInRule) { $matchingBuiltInRule.Priority } else { $associatedRule.Priority } - Enabled = if ($matchingBuiltInRule) { $matchingBuiltInRule.Enabled } else { $associatedRule.Enabled } + State = if ($matchingBuiltInRule) { $matchingBuiltInRule.State } else { $associatedRule.State } SentTo = $associatedRule.SentTo SentToMemberOf = $associatedRule.SentToMemberOf RecipientDomainIs = $associatedRule.RecipientDomainIs @@ -112,7 +113,7 @@ Function Invoke-ListSafeLinksPolicy { PolicyName = $rule.SafeLinksPolicy RuleName = $rule.Name Priority = $rule.Priority - Enabled = $rule.Enabled + State = $rule.State SentTo = $rule.SentTo SentToMemberOf = $rule.SentToMemberOf RecipientDomainIs = $rule.RecipientDomainIs @@ -153,7 +154,7 @@ Function Invoke-ListSafeLinksPolicy { PolicyName = $null RuleName = $builtInRule.Name Priority = $builtInRule.Priority - Enabled = $builtInRule.Enabled + State = $builtInRule.State SentTo = $builtInRule.SentTo SentToMemberOf = $builtInRule.SentToMemberOf RecipientDomainIs = $builtInRule.RecipientDomainIs From f61ef18c43c6f4ada591a87682de5538869370db Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 3 Jun 2025 09:40:21 +0800 Subject: [PATCH 027/160] Added IsValid --- .../Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicy.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicy.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicy.ps1 index 22c07bb966d9..f983a404931b 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicy.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicy.ps1 @@ -83,6 +83,7 @@ Function Invoke-ListSafeLinksPolicy { ExceptIfRecipientDomainIs = $associatedRule.ExceptIfRecipientDomainIs Description = $policy.AdminDisplayName IsBuiltIn = ($matchingBuiltInRule -ne $null) + IsValid = $policy.IsValid ConfigurationStatus = if ($associatedRule) { "Complete" } else { "Policy Only (Missing Rule)" } } $Output.Add($OutputItem) From bc9e588f7292d4b4bab155afd83bc64f9b9b48a4 Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 3 Jun 2025 09:48:22 +0800 Subject: [PATCH 028/160] forgive me for I have sinned --- .../Invoke-EditSafeLinksPolicy.ps1 | 30 +++++++++---------- .../Invoke-ExecDeleteSafeLinksPolicy.ps1 | 14 ++++----- .../Invoke-ExecNewSafeLinksPolicy.ps1 | 16 +++++----- .../Invoke-ListSafeLinksPolicyDetails.ps1 | 14 ++++----- 4 files changed, 37 insertions(+), 37 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-EditSafeLinksPolicy.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-EditSafeLinksPolicy.ps1 index 118b7c22b8c8..c763a14a21e1 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-EditSafeLinksPolicy.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-EditSafeLinksPolicy.ps1 @@ -32,31 +32,31 @@ function Invoke-EditSafeLinksPolicy { # If already an array, process each item if ($Field -is [array]) { - $result = @() + $result = [System.Collections.ArrayList]@() foreach ($item in $Field) { if ($item -is [string]) { - $result += $item + $result.Add($item) | Out-Null } elseif ($item -is [hashtable] -or $item -is [PSCustomObject]) { # Extract value from object if ($null -ne $item.value) { - $result += $item.value + $result.Add($item.value) | Out-Null } elseif ($null -ne $item.userPrincipalName) { - $result += $item.userPrincipalName + $result.Add($item.userPrincipalName) | Out-Null } elseif ($null -ne $item.id) { - $result += $item.id + $result.Add($item.id) | Out-Null } else { - $result += $item.ToString() + $result.Add($item.ToString()) | Out-Null } } else { - $result += $item.ToString() + $result.Add($item.ToString()) | Out-Null } } - return $result + return $result.ToArray() } # If it's a single object @@ -102,11 +102,11 @@ function Invoke-EditSafeLinksPolicy { $ExceptIfSentToMemberOf = Process-ArrayField -Field $Request.Body.ExceptIfSentToMemberOf $ExceptIfRecipientDomainIs = Process-ArrayField -Field $Request.Body.ExceptIfRecipientDomainIs - $Results = [System.Collections.Generic.List[string]]@() + $Results = [System.Collections.ArrayList]@() $hasPolicyParams = $false $hasRuleParams = $false $hasRuleOperation = $false - $ruleMessages = [System.Collections.Generic.List[string]]@() + $ruleMessages = [System.Collections.ArrayList]@() try { # Check which types of updates we need to perform @@ -157,7 +157,7 @@ function Invoke-EditSafeLinksPolicy { } $null = New-ExoRequest @ExoPolicyRequestParam - $Results.Add("Successfully updated SafeLinks policy '$PolicyName'") + $Results.Add("Successfully updated SafeLinks policy '$PolicyName'") | Out-Null Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Updated SafeLinks policy '$PolicyName'" -Sev 'Info' } @@ -172,7 +172,7 @@ function Invoke-EditSafeLinksPolicy { $null = New-ExoRequest @ExoRuleRequestParam $hasRuleOperation = $true - $ruleMessages.Add("updated properties") + $ruleMessages.Add("updated properties") | Out-Null } # Handle enable/disable if needed @@ -190,13 +190,13 @@ function Invoke-EditSafeLinksPolicy { $null = New-ExoRequest @EnableRequestParam $hasRuleOperation = $true $State = $State ? "enabled" : "disabled" - $ruleMessages.Add($State) + $ruleMessages.Add($State) | Out-Null } # Add combined rule message if any rule operations were performed if ($hasRuleOperation) { $ruleOperations = $ruleMessages -join " and " - $Results.Add("Successfully $ruleOperations SafeLinks rule '$RuleName'") + $Results.Add("Successfully $ruleOperations SafeLinks rule '$RuleName'") | Out-Null Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "$ruleOperations SafeLinks rule '$RuleName'" -Sev 'Info' } @@ -204,7 +204,7 @@ function Invoke-EditSafeLinksPolicy { } catch { $ErrorMessage = Get-CippException -Exception $_ - $Results.Add("Failed updating SafeLinks configuration '$PolicyName'. Error: $($ErrorMessage.NormalizedError)") + $Results.Add("Failed updating SafeLinks configuration '$PolicyName'. Error: $($ErrorMessage.NormalizedError)") | Out-Null Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed updating SafeLinks configuration '$PolicyName'. Error: $($ErrorMessage.NormalizedError)" -Sev 'Error' $StatusCode = [HttpStatusCode]::InternalServerError } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecDeleteSafeLinksPolicy.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecDeleteSafeLinksPolicy.ps1 index f14c2050a8e2..6bd622381129 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecDeleteSafeLinksPolicy.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecDeleteSafeLinksPolicy.ps1 @@ -18,7 +18,7 @@ function Invoke-ExecDeleteSafeLinksPolicy { $RuleName = $Request.Query.RuleName ?? $Request.Body.RuleName $PolicyName = $Request.Query.PolicyName ?? $Request.Body.PolicyName - $ResultMessages = @() + $ResultMessages = [System.Collections.ArrayList]@() try { # Only try to delete the rule if a name was provided @@ -34,17 +34,17 @@ function Invoke-ExecDeleteSafeLinksPolicy { useSystemMailbox = $true } $null = New-ExoRequest @ExoRequestRuleParam - $ResultMessages += "Successfully deleted SafeLinks rule '$RuleName'" + $ResultMessages.Add("Successfully deleted SafeLinks rule '$RuleName'") | Out-Null Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Successfully deleted SafeLinks rule '$RuleName'" -Sev 'Info' } catch { $ErrorMessage = Get-CippException -Exception $_ - $ResultMessages += "Failed to delete SafeLinks rule '$RuleName'. Error: $($ErrorMessage.NormalizedError)" + $ResultMessages.Add("Failed to delete SafeLinks rule '$RuleName'. Error: $($ErrorMessage.NormalizedError)") | Out-Null Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to delete SafeLinks rule '$RuleName'. Error: $($ErrorMessage.NormalizedError)" -Sev 'Warning' } } else { - $ResultMessages += "No rule name provided, skipping rule deletion" + $ResultMessages.Add("No rule name provided, skipping rule deletion") | Out-Null Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "No rule name provided, skipping rule deletion" -Sev 'Info' } @@ -61,17 +61,17 @@ function Invoke-ExecDeleteSafeLinksPolicy { useSystemMailbox = $true } $null = New-ExoRequest @ExoRequestPolicyParam - $ResultMessages += "Successfully deleted SafeLinks policy '$PolicyName'" + $ResultMessages.Add("Successfully deleted SafeLinks policy '$PolicyName'") | Out-Null Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Successfully deleted SafeLinks policy '$PolicyName'" -Sev 'Info' } catch { $ErrorMessage = Get-CippException -Exception $_ - $ResultMessages += "Failed to delete SafeLinks policy '$PolicyName'. Error: $($ErrorMessage.NormalizedError)" + $ResultMessages.Add("Failed to delete SafeLinks policy '$PolicyName'. Error: $($ErrorMessage.NormalizedError)") | Out-Null Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to delete SafeLinks policy '$PolicyName'. Error: $($ErrorMessage.NormalizedError)" -Sev 'Warning' } } else { - $ResultMessages += "No policy name provided, skipping policy deletion" + $ResultMessages.Add("No policy name provided, skipping policy deletion") | Out-Null Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "No policy name provided, skipping policy deletion" -Sev 'Info' } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecNewSafeLinksPolicy.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecNewSafeLinksPolicy.ps1 index 8cc0c5c34cf3..91945d8736ca 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecNewSafeLinksPolicy.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecNewSafeLinksPolicy.ps1 @@ -72,31 +72,31 @@ function Invoke-ExecNewSafeLinksPolicy { # If already an array, process each item if ($Field -is [array]) { - $result = @() + $result = [System.Collections.ArrayList]@() foreach ($item in $Field) { if ($item -is [string]) { - $result += $item + $result.Add($item) | Out-Null } elseif ($item -is [hashtable] -or $item -is [PSCustomObject]) { # Extract value from object if ($null -ne $item.value) { - $result += $item.value + $result.Add($item.value) | Out-Null } elseif ($null -ne $item.userPrincipalName) { - $result += $item.userPrincipalName + $result.Add($item.userPrincipalName) | Out-Null } elseif ($null -ne $item.id) { - $result += $item.id + $result.Add($item.id) | Out-Null } else { - $result += $item.ToString() + $result.Add($item.ToString()) | Out-Null } } else { - $result += $item.ToString() + $result.Add($item.ToString()) | Out-Null } } - return $result + return $result.ToArray() } # If it's a single object diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicyDetails.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicyDetails.ps1 index ecb731c415a9..c0b11a9c3ad9 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicyDetails.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicyDetails.ps1 @@ -21,7 +21,7 @@ function Invoke-ListSafeLinksPolicyDetails { $RuleName = $Request.Query.RuleName ?? $Request.Body.RuleName $Result = @{} - $LogMessages = @() + $LogMessages = [System.Collections.ArrayList]@() try { # Get policy details if PolicyName is provided @@ -38,18 +38,18 @@ function Invoke-ListSafeLinksPolicyDetails { $PolicyDetails = New-ExoRequest @PolicyRequestParam $Result.Policy = $PolicyDetails $Result.PolicyName = $PolicyDetails.Name - $LogMessages += "Successfully retrieved details for SafeLinks policy '$PolicyName'" + $LogMessages.Add("Successfully retrieved details for SafeLinks policy '$PolicyName'") | Out-Null Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Successfully retrieved details for SafeLinks policy '$PolicyName'" -Sev 'Info' } catch { $ErrorMessage = Get-CippException -Exception $_ - $LogMessages += "Failed to retrieve details for SafeLinks policy '$PolicyName'. Error: $($ErrorMessage.NormalizedError)" + $LogMessages.Add("Failed to retrieve details for SafeLinks policy '$PolicyName'. Error: $($ErrorMessage.NormalizedError)") | Out-Null Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to retrieve details for SafeLinks policy '$PolicyName'. Error: $($ErrorMessage.NormalizedError)" -Sev 'Warning' $Result.PolicyError = "Failed to retrieve: $($ErrorMessage.NormalizedError)" } } else { - $LogMessages += "No policy name provided, skipping policy retrieval" + $LogMessages.Add("No policy name provided, skipping policy retrieval") | Out-Null Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "No policy name provided, skipping policy retrieval" -Sev 'Info' } @@ -67,18 +67,18 @@ function Invoke-ListSafeLinksPolicyDetails { $RuleDetails = New-ExoRequest @RuleRequestParam $Result.Rule = $RuleDetails $Result.RuleName = $RuleDetails.Name - $LogMessages += "Successfully retrieved details for SafeLinks rule '$RuleName'" + $LogMessages.Add("Successfully retrieved details for SafeLinks rule '$RuleName'") | Out-Null Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Successfully retrieved details for SafeLinks rule '$RuleName'" -Sev 'Info' } catch { $ErrorMessage = Get-CippException -Exception $_ - $LogMessages += "Failed to retrieve details for SafeLinks rule '$RuleName'. Error: $($ErrorMessage.NormalizedError)" + $LogMessages.Add("Failed to retrieve details for SafeLinks rule '$RuleName'. Error: $($ErrorMessage.NormalizedError)") | Out-Null Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to retrieve details for SafeLinks rule '$RuleName'. Error: $($ErrorMessage.NormalizedError)" -Sev 'Warning' $Result.RuleError = "Failed to retrieve: $($ErrorMessage.NormalizedError)" } } else { - $LogMessages += "No rule name provided, skipping rule retrieval" + $LogMessages.Add("No rule name provided, skipping rule retrieval") | Out-Null Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "No rule name provided, skipping rule retrieval" -Sev 'Info' } From b86fdd5b2abca92168d9cbe327bfa5d8c560e1fe Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 3 Jun 2025 09:58:00 +0800 Subject: [PATCH 029/160] Template name and description fixes --- .../Safe-Links-Policy/Invoke-AddSafeLinksPolicyTemplate.ps1 | 4 ++-- .../Invoke-CreateSafeLinksPolicyTemplate.ps1 | 4 ++-- .../Safe-Links-Policy/Invoke-EditSafeLinksPolicyTemplate.ps1 | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyTemplate.ps1 index 75ef6af27675..c4e4a467fa0e 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyTemplate.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyTemplate.ps1 @@ -31,8 +31,8 @@ Function Invoke-AddSafeLinksPolicyTemplate { $policyObject = [ordered]@{} # Set name and comments - prioritize template-specific fields - $policyObject["TemplateName"] = $Request.body.Name - $policyObject["TemplateDescription"] = $Request.body.Description + $policyObject["TemplateName"] = $Request.body.TemplateName + $policyObject["TemplateDescription"] = $Request.body.TemplateDescription # For templates, if no specific policy description is provided, use template description as default if ([string]::IsNullOrEmpty($Request.body.AdminDisplayName) -and -not [string]::IsNullOrEmpty($Request.body.Description)) { diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-CreateSafeLinksPolicyTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-CreateSafeLinksPolicyTemplate.ps1 index 9689856a1274..8023f9b62bbb 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-CreateSafeLinksPolicyTemplate.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-CreateSafeLinksPolicyTemplate.ps1 @@ -23,8 +23,8 @@ Function Invoke-CreateSafeLinksPolicyTemplate { $policyObject = [ordered]@{} # Set name and comments first - $policyObject["TemplateName"] = $Request.body.Name - $policyObject["TemplateDescription"] = $Request.body.Description + $policyObject["TemplateName"] = $Request.body.TemplateName + $policyObject["TemplateDescription"] = $Request.body.TemplateDescription # Copy specific properties we want to keep $propertiesToKeep = @( diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-EditSafeLinksPolicyTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-EditSafeLinksPolicyTemplate.ps1 index dc9bbf0fad5b..3e6ece129dc8 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-EditSafeLinksPolicyTemplate.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-EditSafeLinksPolicyTemplate.ps1 @@ -36,8 +36,8 @@ Function Invoke-EditSafeLinksPolicyTemplate { $policyObject = [ordered]@{} # Set name and comments - $policyObject["TemplateName"] = $Request.body.Name - $policyObject["TemplateDescription"] = $Request.body.Description + $policyObject["TemplateName"] = $Request.body.TemplateName + $policyObject["TemplateDescription"] = $Request.body.TemplateDescription # Copy specific properties we want to keep $propertiesToKeep = @( From 567889d2518f63dbb2569f79d05942fb13e36aec Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 3 Jun 2025 10:18:56 +0800 Subject: [PATCH 030/160] remove logging --- .../Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicy.ps1 | 3 --- 1 file changed, 3 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicy.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicy.ps1 index f983a404931b..a18d03c2aba1 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicy.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicy.ps1 @@ -19,7 +19,6 @@ Function Invoke-ListSafeLinksPolicy { $Policies = New-ExoRequest -tenantid $Tenantfilter -cmdlet 'Get-SafeLinksPolicy' | Select-Object -Property * -ExcludeProperty '*@odata.type' , '*@data.type' $Rules = New-ExoRequest -tenantid $Tenantfilter -cmdlet 'Get-SafeLinksRule' | Select-Object -Property * -ExcludeProperty '*@odata.type' , '*@data.type' $BuiltInRules = New-ExoRequest -tenantid $Tenantfilter -cmdlet 'Get-EOPProtectionPolicyRule' | Select-Object -Property * -ExcludeProperty '*@odata.type' , '*@data.type' - write-host $Rules # Track matched items to identify orphans $MatchedRules = [System.Collections.Generic.HashSet[string]]::new() @@ -180,8 +179,6 @@ Function Invoke-ListSafeLinksPolicy { $RuleOnlyConfigs = ($SortedOutput | Where-Object { $_.ConfigurationStatus -like "*Rule Only*" }).Count $BuiltInOnlyConfigs = ($SortedOutput | Where-Object { $_.ConfigurationStatus -like "*Built-In Rule Only*" }).Count - Write-LogMessage -headers $Headers -API $APIName -message "Retrieved SafeLinks configurations: $CompleteConfigs complete, $PolicyOnlyConfigs policy-only, $RuleOnlyConfigs rule-only, $BuiltInOnlyConfigs built-in-only" -Sev 'Info' - if ($PolicyOnlyConfigs -gt 0 -or $RuleOnlyConfigs -gt 0) { Write-LogMessage -headers $Headers -API $APIName -message "Found $($PolicyOnlyConfigs + $RuleOnlyConfigs) orphaned SafeLinks configurations that may need attention" -Sev 'Warning' } From 57b6ad685e05fd9ba7e07405f0f8527ada9f9849 Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 3 Jun 2025 01:35:56 +0800 Subject: [PATCH 031/160] Safe Links Policy - Management, Standards, and Templates --- Config/standards.json | 29 + .../Reports/Invoke-ListSafeLinksFilters.ps1 | 31 - .../Spamfilter/Invoke-EditSafeLinksFilter.ps1 | 57 -- .../Invoke-AddSafeLinksPolicyFromTemplate.ps1 | 227 ++++++ .../Invoke-AddSafeLinksPolicyTemplate.ps1 | 96 +++ .../Invoke-CreateSafeLinksPolicyTemplate.ps1 | 77 ++ .../Invoke-EditSafeLinksPolicy.ps1 | 217 ++++++ .../Invoke-EditSafeLinksPolicyTemplate.ps1 | 89 +++ .../Invoke-ExecDeleteSafeLinksPolicy.ps1 | 94 +++ .../Invoke-ExecNewSafeLinksPolicy.ps1 | 228 ++++++ .../Invoke-ListSafeLinksPolicy.ps1 | 201 ++++++ .../Invoke-ListSafeLinksPolicyDetails.ps1 | 106 +++ ...oke-ListSafeLinksPolicyTemplateDetails.ps1 | 57 ++ .../Invoke-ListSafeLinksPolicyTemplates.ps1 | 39 + .../Invoke-RemoveSafeLinksPolicyTemplate.ps1 | 35 + ...ke-CIPPStandardSafeLinksTemplatePolicy.ps1 | 489 +++++++++++++ openapi.json | 674 ++++++++++++++++++ 17 files changed, 2658 insertions(+), 88 deletions(-) delete mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListSafeLinksFilters.ps1 delete mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-EditSafeLinksFilter.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyFromTemplate.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyTemplate.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-CreateSafeLinksPolicyTemplate.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-EditSafeLinksPolicy.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-EditSafeLinksPolicyTemplate.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecDeleteSafeLinksPolicy.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecNewSafeLinksPolicy.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicy.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicyDetails.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicyTemplateDetails.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicyTemplates.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-RemoveSafeLinksPolicyTemplate.ps1 create mode 100644 Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksTemplatePolicy.ps1 diff --git a/Config/standards.json b/Config/standards.json index 92ef2e2806f5..dcc0335ddb71 100644 --- a/Config/standards.json +++ b/Config/standards.json @@ -1722,6 +1722,35 @@ "powershellEquivalent": "New-ProtectionAlert and Set-ProtectionAlert", "recommendedBy": [] }, + { + "name": "standards.SafeLinksTemplatePolicy", + "label": "SafeLinks Policy Template", + "cat": "Templates", + "multiple": false, + "disabledFeatures": { + "report": false, + "warn": false, + "remediate": false + }, + "impact": "Medium Impact", + "addedDate": "2025-04-29", + "helpText": "Deploy and manage SafeLinks policy templates to protect against malicious URLs in emails and Office documents.", + "addedComponent": [ + { + "type": "autoComplete", + "multiple": true, + "creatable": false, + "name": "standards.SafeLinksTemplatePolicy.TemplateIds", + "label": "Select SafeLinks Policy Templates", + "api": { + "url": "/api/ListSafeLinksPolicyTemplates", + "labelField": "TemplateName", + "valueField": "GUID", + "queryKey": "ListSafeLinksPolicyTemplates" + } + } + ] + }, { "name": "standards.SafeLinksPolicy", "cat": "Defender Standards", diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListSafeLinksFilters.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListSafeLinksFilters.ps1 deleted file mode 100644 index c9e395c05de7..000000000000 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListSafeLinksFilters.ps1 +++ /dev/null @@ -1,31 +0,0 @@ -function Invoke-ListSafeLinksFilters { - <# - .FUNCTIONALITY - Entrypoint - .ROLE - Exchange.SpamFilter.Read - #> - [CmdletBinding()] - param($Request, $TriggerMetadata) - - $APIName = $Request.Params.CIPPEndpoint - $Headers = $Request.Headers - Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' - - # Interact with query parameters or the body of the request. - $TenantFilter = $Request.Query.TenantFilter - $Policies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-SafeLinksPolicy' | Select-Object -Property * - $Rules = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-SafeLinksRule' | Select-Object -Property * - - $Output = $Policies | Select-Object -Property *, - @{ Name = 'RuleName'; Expression = { foreach ($item in $Rules) { if ($item.SafeLinksPolicy -eq $_.Name) { $item.Name } } } }, - @{ Name = 'Priority'; Expression = { foreach ($item in $Rules) { if ($item.SafeLinksPolicy -eq $_.Name) { $item.Priority } } } }, - @{ Name = 'RecipientDomainIs'; Expression = { foreach ($item in $Rules) { if ($item.SafeLinksPolicy -eq $_.Name) { $item.RecipientDomainIs } } } }, - @{ Name = 'State'; Expression = { foreach ($item in $Rules) { if ($item.SafeLinksPolicy -eq $_.Name) { $item.State } } } } - - # Associate values to output bindings by calling 'Push-OutputBinding'. - Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ - StatusCode = [HttpStatusCode]::OK - Body = $Output - }) -} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-EditSafeLinksFilter.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-EditSafeLinksFilter.ps1 deleted file mode 100644 index fd7b11144b48..000000000000 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-EditSafeLinksFilter.ps1 +++ /dev/null @@ -1,57 +0,0 @@ -function Invoke-EditSafeLinksFilter { - <# - .FUNCTIONALITY - Entrypoint - .ROLE - Exchange.SpamFilter.Read - #> - [CmdletBinding()] - param($Request, $TriggerMetadata) - - $APIName = $Request.Params.CIPPEndpoint - $Headers = $Request.Headers - Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' - - # Interact with query parameters or the body of the request. - $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter - $RuleName = $Request.Query.RuleName ?? $Request.Body.RuleName - $State = $Request.Query.State ?? $Request.Body.State - - try { - $ExoRequestParam = @{ - tenantid = $TenantFilter - cmdParams = @{ - Identity = $RuleName - } - useSystemMailbox = $true - } - - switch ($State) { - 'Enable' { - $ExoRequestParam.Add('cmdlet', 'Enable-SafeLinksRule') - } - 'Disable' { - $ExoRequestParam.Add('cmdlet', 'Disable-SafeLinksRule') - } - Default { - throw 'Invalid state' - } - } - $null = New-ExoRequest @ExoRequestParam - - $Result = "Successfully set SafeLinks rule $($RuleName) to $($State)" - Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Info - $StatusCode = [HttpStatusCode]::OK - } catch { - $ErrorMessage = Get-CippException -Exception $_ - $Result = "Failed setting SafeLinks rule $($RuleName) to $($State). Error: $($ErrorMessage.NormalizedError)" - Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev 'Error' - $StatusCode = [HttpStatusCode]::InternalServerError - } - - # Associate values to output bindings by calling 'Push-OutputBinding'. - Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ - StatusCode = $StatusCode - Body = @{Results = $Result } - }) -} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyFromTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyFromTemplate.ps1 new file mode 100644 index 000000000000..fe506d605367 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyFromTemplate.ps1 @@ -0,0 +1,227 @@ +using namespace System.Net + +Function Invoke-AddSafeLinksPolicyFromTemplate { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Exchange.SafeLinks.ReadWrite + .DESCRIPTION + This function deploys SafeLinks policies and rules from templates to selected tenants. + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + + try { + $RequestBody = $Request.Body + + # Extract tenant IDs from selectedTenants + $SelectedTenants = $RequestBody.selectedTenants | ForEach-Object { $_.value } + if ('AllTenants' -in $SelectedTenants) { + $SelectedTenants = (Get-Tenants).defaultDomainName + } + + # Extract templates from TemplateList + $Templates = $RequestBody.TemplateList | ForEach-Object { $_.value } + + if (-not $Templates -or $Templates.Count -eq 0) { + throw "No templates provided in TemplateList" + } + + # Helper function to process array fields with cleaner logic + function ConvertTo-SafeArray { + param($Field) + + if ($null -eq $Field) { return @() } + + # Handle arrays + if ($Field -is [array]) { + return $Field | ForEach-Object { + if ($_ -is [string]) { $_ } + elseif ($_.value) { $_.value } + elseif ($_.userPrincipalName) { $_.userPrincipalName } + elseif ($_.id) { $_.id } + else { $_.ToString() } + } + } + + # Handle single objects + if ($Field -is [hashtable] -or $Field -is [PSCustomObject]) { + if ($Field.value) { return @($Field.value) } + if ($Field.userPrincipalName) { return @($Field.userPrincipalName) } + if ($Field.id) { return @($Field.id) } + } + + # Handle strings + if ($Field -is [string]) { return @($Field) } + + return @($Field) + } + + function Test-PolicyExists { + param($TenantFilter, $PolicyName) + + $ExistingPolicies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-SafeLinksPolicy' -useSystemMailbox $true + return $ExistingPolicies | Where-Object { $_.Name -eq $PolicyName } + } + + function Test-RuleExists { + param($TenantFilter, $RuleName) + + $ExistingRules = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-SafeLinksRule' -useSystemMailbox $true + return $ExistingRules | Where-Object { $_.Name -eq $RuleName } + } + + function New-SafeLinksPolicyFromTemplate { + param($TenantFilter, $Template) + + $PolicyName = $Template.PolicyName + $RuleName = $Template.RuleName ?? "$($PolicyName)_Rule" + + # Check if policy already exists + if (Test-PolicyExists -TenantFilter $TenantFilter -PolicyName $PolicyName) { + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Policy '$PolicyName' already exists" -Sev 'Warning' + return "Policy '$PolicyName' already exists in tenant $TenantFilter" + } + + # Check if rule already exists + if (Test-RuleExists -TenantFilter $TenantFilter -RuleName $RuleName) { + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Rule '$RuleName' already exists" -Sev 'Warning' + return "Rule '$RuleName' already exists in tenant $TenantFilter" + } + + # Process array fields + $DoNotRewriteUrls = ConvertTo-SafeArray -Field $Template.DoNotRewriteUrls + $SentTo = ConvertTo-SafeArray -Field $Template.SentTo + $SentToMemberOf = ConvertTo-SafeArray -Field $Template.SentToMemberOf + $RecipientDomainIs = ConvertTo-SafeArray -Field $Template.RecipientDomainIs + $ExceptIfSentTo = ConvertTo-SafeArray -Field $Template.ExceptIfSentTo + $ExceptIfSentToMemberOf = ConvertTo-SafeArray -Field $Template.ExceptIfSentToMemberOf + $ExceptIfRecipientDomainIs = ConvertTo-SafeArray -Field $Template.ExceptIfRecipientDomainIs + + # Create policy parameters + $PolicyParams = @{ Name = $PolicyName } + + # Policy configuration mapping + $PolicyMappings = @{ + 'EnableSafeLinksForEmail' = 'EnableSafeLinksForEmail' + 'EnableSafeLinksForTeams' = 'EnableSafeLinksForTeams' + 'EnableSafeLinksForOffice' = 'EnableSafeLinksForOffice' + 'TrackClicks' = 'TrackClicks' + 'AllowClickThrough' = 'AllowClickThrough' + 'ScanUrls' = 'ScanUrls' + 'EnableForInternalSenders' = 'EnableForInternalSenders' + 'DeliverMessageAfterScan' = 'DeliverMessageAfterScan' + 'DisableUrlRewrite' = 'DisableUrlRewrite' + 'AdminDisplayName' = 'AdminDisplayName' + 'CustomNotificationText' = 'CustomNotificationText' + 'EnableOrganizationBranding' = 'EnableOrganizationBranding' + } + + foreach ($templateKey in $PolicyMappings.Keys) { + if ($null -ne $Template.$templateKey) { + $PolicyParams[$PolicyMappings[$templateKey]] = $Template.$templateKey + } + } + + if ($DoNotRewriteUrls.Count -gt 0) { + $PolicyParams['DoNotRewriteUrls'] = $DoNotRewriteUrls + } + + # Create SafeLinks Policy + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'New-SafeLinksPolicy' -cmdParams $PolicyParams -useSystemMailbox $true + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Created SafeLinks policy '$PolicyName'" -Sev 'Info' + + # Create rule parameters + $RuleParams = @{ + Name = $RuleName + SafeLinksPolicy = $PolicyName + } + + # Rule configuration mapping + $RuleMappings = @{ + 'Priority' = 'Priority' + 'TemplateDescription' = 'Comments' + } + + foreach ($templateKey in $RuleMappings.Keys) { + if ($null -ne $Template.$templateKey) { + $RuleParams[$RuleMappings[$templateKey]] = $Template.$templateKey + } + } + + # Add array parameters if they have values + $ArrayMappings = @{ + 'SentTo' = $SentTo + 'SentToMemberOf' = $SentToMemberOf + 'RecipientDomainIs' = $RecipientDomainIs + 'ExceptIfSentTo' = $ExceptIfSentTo + 'ExceptIfSentToMemberOf' = $ExceptIfSentToMemberOf + 'ExceptIfRecipientDomainIs' = $ExceptIfRecipientDomainIs + } + + foreach ($paramName in $ArrayMappings.Keys) { + if ($ArrayMappings[$paramName].Count -gt 0) { + $RuleParams[$paramName] = $ArrayMappings[$paramName] + } + } + + # Create SafeLinks Rule + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'New-SafeLinksRule' -cmdParams $RuleParams -useSystemMailbox $true + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Created SafeLinks rule '$RuleName'" -Sev 'Info' + + # Handle rule state + $StateMessage = "" + if ($null -ne $Template.State) { + $IsState = switch ($Template.State) { + "Enabled" { $true } + "Disabled" { $false } + $true { $true } + $false { $false } + default { $null } + } + + if ($null -ne $IsState) { + $Cmdlet = $IsState ? 'Enable-SafeLinksRule' : 'Disable-SafeLinksRule' + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet $Cmdlet -cmdParams @{ Identity = $RuleName } -useSystemMailbox $true + $StateMessage = " (rule $($IsState ? 'enabled' : 'disabled'))" + } + } + + return "Successfully deployed SafeLinks policy '$PolicyName' and rule '$RuleName' to tenant $TenantFilter$StateMessage" + } + + # Process each tenant and template combination + $Results = foreach ($TenantFilter in $SelectedTenants) { + foreach ($Template in $Templates) { + try { + New-SafeLinksPolicyFromTemplate -TenantFilter $TenantFilter -Template $Template + } + catch { + $ErrorMessage = Get-CippException -Exception $_ + $ErrorDetail = "Failed to deploy template '$($Template.TemplateName)' to tenant $TenantFilter. Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $ErrorDetail -Sev 'Error' + $ErrorDetail + } + } + } + + $StatusCode = [HttpStatusCode]::OK + } + catch { + $ErrorMessage = Get-CippException -Exception $_ + $Results = "Failed to process template deployment request. Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -message $Results -Sev 'Error' + $StatusCode = [HttpStatusCode]::InternalServerError + } + + # Return response + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{ Results = $Results } + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyTemplate.ps1 new file mode 100644 index 000000000000..c4e4a467fa0e --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyTemplate.ps1 @@ -0,0 +1,96 @@ +using namespace System.Net +Function Invoke-AddSafeLinksPolicyTemplate { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + Exchange.SafeLinks.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -Headers $Headers -API $APINAME -message 'Accessed this API' -Sev Debug + + # Debug: Log the incoming request body + Write-LogMessage -Headers $Headers -API $APINAME -message "Request body: $($Request.body | ConvertTo-Json -Depth 5 -Compress)" -Sev Debug + + try { + $GUID = (New-Guid).GUID + + # Validate required fields + if ([string]::IsNullOrEmpty($Request.body.Name)) { + throw "Template name is required but was not provided" + } + + if ([string]::IsNullOrEmpty($Request.body.PolicyName)) { + throw "Policy name is required but was not provided" + } + + # Create a new ordered hashtable to store selected properties + $policyObject = [ordered]@{} + + # Set name and comments - prioritize template-specific fields + $policyObject["TemplateName"] = $Request.body.TemplateName + $policyObject["TemplateDescription"] = $Request.body.TemplateDescription + + # For templates, if no specific policy description is provided, use template description as default + if ([string]::IsNullOrEmpty($Request.body.AdminDisplayName) -and -not [string]::IsNullOrEmpty($Request.body.Description)) { + $Request.body.AdminDisplayName = $Request.body.Description + Write-LogMessage -Headers $Headers -API $APINAME -message "Using template description as default policy description" -Sev Debug + } + + # Log what we're using for template name and description + Write-LogMessage -Headers $Headers -API $APINAME -message "Template Name: '$($policyObject.TemplateName)', Description: '$($policyObject.TemplateDescription)'" -Sev Debug + + # Copy specific properties we want to keep + $propertiesToKeep = @( + # Policy properties + "PolicyName", "EnableSafeLinksForEmail", "EnableSafeLinksForTeams", "EnableSafeLinksForOffice", + "TrackClicks", "AllowClickThrough", "ScanUrls", "EnableForInternalSenders", + "DeliverMessageAfterScan", "DisableUrlRewrite", "DoNotRewriteUrls", + "AdminDisplayName", "CustomNotificationText", "EnableOrganizationBranding", + # Rule properties + "RuleName", "Priority", "State", "Comments", + "SentTo", "SentToMemberOf", "RecipientDomainIs", + "ExceptIfSentTo", "ExceptIfSentToMemberOf", "ExceptIfRecipientDomainIs" + ) + + # Copy each property if it exists + foreach ($prop in $propertiesToKeep) { + if ($null -ne $Request.body.$prop) { + $policyObject[$prop] = $Request.body.$prop + Write-LogMessage -Headers $Headers -API $APINAME -message "Added property '$prop' with value '$($Request.body.$prop)'" -Sev Debug + } + } + + # Convert to JSON + $JSON = $policyObject | ConvertTo-Json -Depth 10 + Write-LogMessage -Headers $Headers -API $APINAME -message "Final JSON: $JSON" -Sev Debug + + # Save the template to Azure Table Storage + $Table = Get-CippTable -tablename 'templates' + $Table.Force = $true + Add-CIPPAzDataTableEntity @Table -Entity @{ + JSON = "$JSON" + RowKey = "$GUID" + PartitionKey = 'SafeLinksTemplate' + } + + Write-LogMessage -Headers $Headers -API $APINAME -message "Created SafeLinks Policy Template '$($policyObject.TemplateName)' with GUID $GUID" -Sev Info + $body = [pscustomobject]@{'Results' = "Created SafeLinks Policy Template '$($policyObject.TemplateName)' with GUID $GUID" } + $StatusCode = [HttpStatusCode]::OK + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -Headers $Headers -API $APINAME -message "Failed to create SafeLinks policy template: $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage + $body = [pscustomobject]@{'Results' = "Failed to create SafeLinks policy template: $($ErrorMessage.NormalizedError)" } + $StatusCode = [HttpStatusCode]::Forbidden + } + + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = $body + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-CreateSafeLinksPolicyTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-CreateSafeLinksPolicyTemplate.ps1 new file mode 100644 index 000000000000..8023f9b62bbb --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-CreateSafeLinksPolicyTemplate.ps1 @@ -0,0 +1,77 @@ +using namespace System.Net + +Function Invoke-CreateSafeLinksPolicyTemplate { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + Exchange.SafeLinks.ReadWrite + .DESCRIPTION + This function creates a new Safe Links policy template from scratch. + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -Headers $Headers -API $APINAME -message 'Accessed this API' -Sev Debug + + try { + $GUID = (New-Guid).GUID + + # Create a new ordered hashtable to store selected properties + $policyObject = [ordered]@{} + + # Set name and comments first + $policyObject["TemplateName"] = $Request.body.TemplateName + $policyObject["TemplateDescription"] = $Request.body.TemplateDescription + + # Copy specific properties we want to keep + $propertiesToKeep = @( + # Policy properties + "PolicyName", "EnableSafeLinksForEmail", "EnableSafeLinksForTeams", "EnableSafeLinksForOffice", + "TrackClicks", "AllowClickThrough", "ScanUrls", "EnableForInternalSenders", + "DeliverMessageAfterScan", "DisableUrlRewrite", "DoNotRewriteUrls", + "AdminDisplayName", "CustomNotificationText", "EnableOrganizationBranding", + + # Rule properties + "RuleName", "Priority", "State", "Comments", + "SentTo", "SentToMemberOf", "RecipientDomainIs", + "ExceptIfSentTo", "ExceptIfSentToMemberOf", "ExceptIfRecipientDomainIs" + ) + + # Copy each property if it exists + foreach ($prop in $propertiesToKeep) { + if ($null -ne $Request.body.$prop) { + $policyObject[$prop] = $Request.body.$prop + } + } + + # Convert to JSON + $JSON = $policyObject | ConvertTo-Json -Depth 10 + + # Save the template to Azure Table Storage + $Table = Get-CippTable -tablename 'templates' + $Table.Force = $true + Add-CIPPAzDataTableEntity @Table -Entity @{ + JSON = "$JSON" + RowKey = "$GUID" + PartitionKey = 'SafeLinksTemplate' + } + + Write-LogMessage -Headers $Headers -API $APINAME -message "Created SafeLinks Policy Template $($policyObject.TemplateName) with GUID $GUID" -Sev Info + $body = [pscustomobject]@{'Results' = "Created SafeLinks Policy Template $($policyObject.TemplateName) with GUID $GUID" } + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -Headers $Headers -API $APINAME -message "Failed to create SafeLinks policy template: $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage + $body = [pscustomobject]@{'Results' = "Failed to create SafeLinks policy template: $($ErrorMessage.NormalizedError)" } + $StatusCode = [HttpStatusCode]::Forbidden + } + + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = $body + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-EditSafeLinksPolicy.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-EditSafeLinksPolicy.ps1 new file mode 100644 index 000000000000..c763a14a21e1 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-EditSafeLinksPolicy.ps1 @@ -0,0 +1,217 @@ +using namespace System.Net + +function Invoke-EditSafeLinksPolicy { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Exchange.SpamFilter.ReadWrite + .DESCRIPTION + This function modifies an existing Safe Links policy and its associated rule. + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + + # Interact with query parameters or the body of the request. + $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter + $PolicyName = $Request.Query.PolicyName ?? $Request.Body.PolicyName + $RuleName = $Request.Query.RuleName ?? $Request.Body.RuleName + + # Helper function to process array fields + function Process-ArrayField { + param ( + [Parameter(Mandatory = $false)] + $Field + ) + + if ($null -eq $Field) { return @() } + + # If already an array, process each item + if ($Field -is [array]) { + $result = [System.Collections.ArrayList]@() + foreach ($item in $Field) { + if ($item -is [string]) { + $result.Add($item) | Out-Null + } + elseif ($item -is [hashtable] -or $item -is [PSCustomObject]) { + # Extract value from object + if ($null -ne $item.value) { + $result.Add($item.value) | Out-Null + } + elseif ($null -ne $item.userPrincipalName) { + $result.Add($item.userPrincipalName) | Out-Null + } + elseif ($null -ne $item.id) { + $result.Add($item.id) | Out-Null + } + else { + $result.Add($item.ToString()) | Out-Null + } + } + else { + $result.Add($item.ToString()) | Out-Null + } + } + return $result.ToArray() + } + + # If it's a single object + if ($Field -is [hashtable] -or $Field -is [PSCustomObject]) { + if ($null -ne $Field.value) { return @($Field.value) } + if ($null -ne $Field.userPrincipalName) { return @($Field.userPrincipalName) } + if ($null -ne $Field.id) { return @($Field.id) } + } + + # If it's a string, return as an array with one item + if ($Field -is [string]) { + return @($Field) + } + + return @($Field) + } + + # Extract policy parameters from body + $EnableSafeLinksForEmail = $Request.Body.EnableSafeLinksForEmail + $EnableSafeLinksForTeams = $Request.Body.EnableSafeLinksForTeams + $EnableSafeLinksForOffice = $Request.Body.EnableSafeLinksForOffice + $TrackClicks = $Request.Body.TrackClicks + $AllowClickThrough = $Request.Body.AllowClickThrough + $ScanUrls = $Request.Body.ScanUrls + $EnableForInternalSenders = $Request.Body.EnableForInternalSenders + $DeliverMessageAfterScan = $Request.Body.DeliverMessageAfterScan + $DisableUrlRewrite = $Request.Body.DisableUrlRewrite + $DoNotRewriteUrls = Process-ArrayField -Field $Request.Body.DoNotRewriteUrls + $AdminDisplayName = $Request.Body.AdminDisplayName + $CustomNotificationText = $Request.Body.CustomNotificationText + $EnableOrganizationBranding = $Request.Body.EnableOrganizationBranding + + # Extract rule parameters from body + $Priority = $Request.Body.Priority + $Comments = $Request.Body.Comments + $State = $Request.Body.State + + # Process recipient-related parameters + $SentTo = Process-ArrayField -Field $Request.Body.SentTo + $SentToMemberOf = Process-ArrayField -Field $Request.Body.SentToMemberOf + $RecipientDomainIs = Process-ArrayField -Field $Request.Body.RecipientDomainIs + $ExceptIfSentTo = Process-ArrayField -Field $Request.Body.ExceptIfSentTo + $ExceptIfSentToMemberOf = Process-ArrayField -Field $Request.Body.ExceptIfSentToMemberOf + $ExceptIfRecipientDomainIs = Process-ArrayField -Field $Request.Body.ExceptIfRecipientDomainIs + + $Results = [System.Collections.ArrayList]@() + $hasPolicyParams = $false + $hasRuleParams = $false + $hasRuleOperation = $false + $ruleMessages = [System.Collections.ArrayList]@() + + try { + # Check which types of updates we need to perform + # PART 1: Build and check policy parameters + $policyParams = @{ + Identity = $PolicyName + } + + # Only add parameters that are explicitly provided + if ($null -ne $EnableSafeLinksForEmail) { $policyParams.Add('EnableSafeLinksForEmail', $EnableSafeLinksForEmail); $hasPolicyParams = $true } + if ($null -ne $EnableSafeLinksForTeams) { $policyParams.Add('EnableSafeLinksForTeams', $EnableSafeLinksForTeams); $hasPolicyParams = $true } + if ($null -ne $EnableSafeLinksForOffice) { $policyParams.Add('EnableSafeLinksForOffice', $EnableSafeLinksForOffice); $hasPolicyParams = $true } + if ($null -ne $TrackClicks) { $policyParams.Add('TrackClicks', $TrackClicks); $hasPolicyParams = $true } + if ($null -ne $AllowClickThrough) { $policyParams.Add('AllowClickThrough', $AllowClickThrough); $hasPolicyParams = $true } + if ($null -ne $ScanUrls) { $policyParams.Add('ScanUrls', $ScanUrls); $hasPolicyParams = $true } + if ($null -ne $EnableForInternalSenders) { $policyParams.Add('EnableForInternalSenders', $EnableForInternalSenders); $hasPolicyParams = $true } + if ($null -ne $DeliverMessageAfterScan) { $policyParams.Add('DeliverMessageAfterScan', $DeliverMessageAfterScan); $hasPolicyParams = $true } + if ($null -ne $DisableUrlRewrite) { $policyParams.Add('DisableUrlRewrite', $DisableUrlRewrite); $hasPolicyParams = $true } + if ($null -ne $DoNotRewriteUrls -and $DoNotRewriteUrls.Count -gt 0) { $policyParams.Add('DoNotRewriteUrls', $DoNotRewriteUrls); $hasPolicyParams = $true } + if ($null -ne $AdminDisplayName) { $policyParams.Add('AdminDisplayName', $AdminDisplayName); $hasPolicyParams = $true } + if ($null -ne $CustomNotificationText) { $policyParams.Add('CustomNotificationText', $CustomNotificationText); $hasPolicyParams = $true } + if ($null -ne $EnableOrganizationBranding) { $policyParams.Add('EnableOrganizationBranding', $EnableOrganizationBranding); $hasPolicyParams = $true } + + # PART 2: Build and check rule parameters + $ruleParams = @{ + Identity = $RuleName + } + + # Add parameters that are explicitly provided + if ($null -ne $Comments) { $ruleParams.Add('Comments', $Comments); $hasRuleParams = $true } + if ($null -ne $Priority) { $ruleParams.Add('Priority', $Priority); $hasRuleParams = $true } + if ($SentTo.Count -gt 0) { $ruleParams.Add('SentTo', $SentTo); $hasRuleParams = $true } + if ($SentToMemberOf.Count -gt 0) { $ruleParams.Add('SentToMemberOf', $SentToMemberOf); $hasRuleParams = $true } + if ($RecipientDomainIs.Count -gt 0) { $ruleParams.Add('RecipientDomainIs', $RecipientDomainIs); $hasRuleParams = $true } + if ($ExceptIfSentTo.Count -gt 0) { $ruleParams.Add('ExceptIfSentTo', $ExceptIfSentTo); $hasRuleParams = $true } + if ($ExceptIfSentToMemberOf.Count -gt 0) { $ruleParams.Add('ExceptIfSentToMemberOf', $ExceptIfSentToMemberOf); $hasRuleParams = $true } + if ($ExceptIfRecipientDomainIs.Count -gt 0) { $ruleParams.Add('ExceptIfRecipientDomainIs', $ExceptIfRecipientDomainIs); $hasRuleParams = $true } + + # Now perform only the necessary operations + + # PART 1: Update policy if needed + if ($hasPolicyParams) { + $ExoPolicyRequestParam = @{ + tenantid = $TenantFilter + cmdlet = 'Set-SafeLinksPolicy' + cmdParams = $policyParams + useSystemMailbox = $true + } + + $null = New-ExoRequest @ExoPolicyRequestParam + $Results.Add("Successfully updated SafeLinks policy '$PolicyName'") | Out-Null + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Updated SafeLinks policy '$PolicyName'" -Sev 'Info' + } + + # PART 2: Update rule if needed + if ($hasRuleParams) { + $ExoRuleRequestParam = @{ + tenantid = $TenantFilter + cmdlet = 'Set-SafeLinksRule' + cmdParams = $ruleParams + useSystemMailbox = $true + } + + $null = New-ExoRequest @ExoRuleRequestParam + $hasRuleOperation = $true + $ruleMessages.Add("updated properties") | Out-Null + } + + # Handle enable/disable if needed + if ($null -ne $State) { + $EnableCmdlet = $State ? 'Enable-SafeLinksRule' : 'Disable-SafeLinksRule' + $EnableRequestParam = @{ + tenantid = $TenantFilter + cmdlet = $EnableCmdlet + cmdParams = @{ + Identity = $RuleName + } + useSystemMailbox = $true + } + + $null = New-ExoRequest @EnableRequestParam + $hasRuleOperation = $true + $State = $State ? "enabled" : "disabled" + $ruleMessages.Add($State) | Out-Null + } + + # Add combined rule message if any rule operations were performed + if ($hasRuleOperation) { + $ruleOperations = $ruleMessages -join " and " + $Results.Add("Successfully $ruleOperations SafeLinks rule '$RuleName'") | Out-Null + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "$ruleOperations SafeLinks rule '$RuleName'" -Sev 'Info' + } + + $StatusCode = [HttpStatusCode]::OK + } + catch { + $ErrorMessage = Get-CippException -Exception $_ + $Results.Add("Failed updating SafeLinks configuration '$PolicyName'. Error: $($ErrorMessage.NormalizedError)") | Out-Null + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed updating SafeLinks configuration '$PolicyName'. Error: $($ErrorMessage.NormalizedError)" -Sev 'Error' + $StatusCode = [HttpStatusCode]::InternalServerError + } + + # 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/Security/Safe-Links-Policy/Invoke-EditSafeLinksPolicyTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-EditSafeLinksPolicyTemplate.ps1 new file mode 100644 index 000000000000..3e6ece129dc8 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-EditSafeLinksPolicyTemplate.ps1 @@ -0,0 +1,89 @@ +using namespace System.Net + +Function Invoke-EditSafeLinksPolicyTemplate { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + Exchange.SafeLinks.ReadWrite + .DESCRIPTION + This function updates an existing Safe Links policy template. + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -Headers $Headers -API $APINAME -message 'Accessed this API' -Sev Debug + + try { + $ID = $Request.Body.ID + + if (-not $ID) { + throw "Template ID is required" + } + + # Check if template exists + $Table = Get-CippTable -tablename 'templates' + $Filter = "PartitionKey eq 'SafeLinksTemplate' and RowKey eq '$ID'" + $ExistingTemplate = Get-CIPPAzDataTableEntity @Table -Filter $Filter + + if (-not $ExistingTemplate) { + throw "Template with ID '$ID' not found" + } + + # Create a new ordered hashtable to store selected properties + $policyObject = [ordered]@{} + + # Set name and comments + $policyObject["TemplateName"] = $Request.body.TemplateName + $policyObject["TemplateDescription"] = $Request.body.TemplateDescription + + # Copy specific properties we want to keep + $propertiesToKeep = @( + # Policy properties + "PolicyName", "EnableSafeLinksForEmail", "EnableSafeLinksForTeams", "EnableSafeLinksForOffice", + "TrackClicks", "AllowClickThrough", "ScanUrls", "EnableForInternalSenders", + "DeliverMessageAfterScan", "DisableUrlRewrite", "DoNotRewriteUrls", + "AdminDisplayName", "CustomNotificationText", "EnableOrganizationBranding", + + # Rule properties + "RuleName", "Priority", "State", "Comments", + "SentTo", "SentToMemberOf", "RecipientDomainIs", + "ExceptIfSentTo", "ExceptIfSentToMemberOf", "ExceptIfRecipientDomainIs" + ) + + # Copy each property if it exists + foreach ($prop in $propertiesToKeep) { + if ($null -ne $Request.body.$prop) { + $policyObject[$prop] = $Request.body.$prop + } + } + + # Convert to JSON + $JSON = $policyObject | ConvertTo-Json -Depth 10 + + # Update the template in Azure Table Storage + $Table.Force = $true + Add-CIPPAzDataTableEntity @Table -Entity @{ + JSON = "$JSON" + RowKey = "$ID" + PartitionKey = 'SafeLinksTemplate' + } + + Write-LogMessage -Headers $Headers -API $APINAME -message "Updated SafeLinks Policy Template $($policyObject.TemplateName) with ID $ID" -Sev Info + $body = [pscustomobject]@{'Results' = "Updated SafeLinks Policy Template $($policyObject.TemplateName) with ID $ID" } + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -Headers $Headers -API $APINAME -message "Failed to update SafeLinks policy template: $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage + $body = [pscustomobject]@{'Results' = "Failed to update SafeLinks policy template: $($ErrorMessage.NormalizedError)" } + $StatusCode = [HttpStatusCode]::Forbidden + } + + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = $body + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecDeleteSafeLinksPolicy.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecDeleteSafeLinksPolicy.ps1 new file mode 100644 index 000000000000..6bd622381129 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecDeleteSafeLinksPolicy.ps1 @@ -0,0 +1,94 @@ +using namespace System.Net +function Invoke-ExecDeleteSafeLinksPolicy { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Exchange.SpamFilter.ReadWrite + .DESCRIPTION + This function deletes a Safe Links rule and its associated policy. + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + # Interact with query parameters or the body of the request. + $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter + $RuleName = $Request.Query.RuleName ?? $Request.Body.RuleName + $PolicyName = $Request.Query.PolicyName ?? $Request.Body.PolicyName + + $ResultMessages = [System.Collections.ArrayList]@() + + try { + # Only try to delete the rule if a name was provided + if ($RuleName) { + try { + $ExoRequestRuleParam = @{ + tenantid = $TenantFilter + cmdlet = 'Remove-SafeLinksRule' + cmdParams = @{ + Identity = $RuleName + Confirm = $false + } + useSystemMailbox = $true + } + $null = New-ExoRequest @ExoRequestRuleParam + $ResultMessages.Add("Successfully deleted SafeLinks rule '$RuleName'") | Out-Null + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Successfully deleted SafeLinks rule '$RuleName'" -Sev 'Info' + } + catch { + $ErrorMessage = Get-CippException -Exception $_ + $ResultMessages.Add("Failed to delete SafeLinks rule '$RuleName'. Error: $($ErrorMessage.NormalizedError)") | Out-Null + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to delete SafeLinks rule '$RuleName'. Error: $($ErrorMessage.NormalizedError)" -Sev 'Warning' + } + } + else { + $ResultMessages.Add("No rule name provided, skipping rule deletion") | Out-Null + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "No rule name provided, skipping rule deletion" -Sev 'Info' + } + + # Only try to delete the policy if a name was provided + if ($PolicyName) { + try { + $ExoRequestPolicyParam = @{ + tenantid = $TenantFilter + cmdlet = 'Remove-SafeLinksPolicy' + cmdParams = @{ + Identity = $PolicyName + Confirm = $false + } + useSystemMailbox = $true + } + $null = New-ExoRequest @ExoRequestPolicyParam + $ResultMessages.Add("Successfully deleted SafeLinks policy '$PolicyName'") | Out-Null + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Successfully deleted SafeLinks policy '$PolicyName'" -Sev 'Info' + } + catch { + $ErrorMessage = Get-CippException -Exception $_ + $ResultMessages.Add("Failed to delete SafeLinks policy '$PolicyName'. Error: $($ErrorMessage.NormalizedError)") | Out-Null + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to delete SafeLinks policy '$PolicyName'. Error: $($ErrorMessage.NormalizedError)" -Sev 'Warning' + } + } + else { + $ResultMessages.Add("No policy name provided, skipping policy deletion") | Out-Null + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "No policy name provided, skipping policy deletion" -Sev 'Info' + } + + # Combine all result messages + $Result = $ResultMessages -join " | " + $StatusCode = [HttpStatusCode]::OK + } + catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "An unexpected error occurred: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev 'Error' + $StatusCode = [HttpStatusCode]::InternalServerError + } + + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{Results = $Result } + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecNewSafeLinksPolicy.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecNewSafeLinksPolicy.ps1 new file mode 100644 index 000000000000..91945d8736ca --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecNewSafeLinksPolicy.ps1 @@ -0,0 +1,228 @@ +using namespace System.Net + +function Invoke-ExecNewSafeLinksPolicy { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Exchange.SpamFilter.ReadWrite + .DESCRIPTION + This function creates a new Safe Links policy and an associated rule. + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + + # Interact with query parameters or the body of the request. + $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter + + # Extract policy settings from body + $PolicyName = $Request.Body.PolicyName + $EnableSafeLinksForEmail = $Request.Body.EnableSafeLinksForEmail + $EnableSafeLinksForTeams = $Request.Body.EnableSafeLinksForTeams + $EnableSafeLinksForOffice = $Request.Body.EnableSafeLinksForOffice + $TrackClicks = $Request.Body.TrackClicks + $AllowClickThrough = $Request.Body.AllowClickThrough + $ScanUrls = $Request.Body.ScanUrls + $EnableForInternalSenders = $Request.Body.EnableForInternalSenders + $DeliverMessageAfterScan = $Request.Body.DeliverMessageAfterScan + $DisableUrlRewrite = $Request.Body.DisableUrlRewrite + $DoNotRewriteUrls = $Request.Body.DoNotRewriteUrls + $AdminDisplayName = $Request.Body.AdminDisplayName + $CustomNotificationText = $Request.Body.CustomNotificationText + $EnableOrganizationBranding = $Request.Body.EnableOrganizationBranding + + # Extract rule settings from body + $Priority = $Request.Body.Priority + $Comments = $Request.Body.Comments + $State = $Request.Body.State + $RuleName = $Request.Body.RuleName + + # Extract recipient fields and handle different input formats + $SentTo = $Request.Body.SentTo + $SentToMemberOf = $Request.Body.SentToMemberOf + $RecipientDomainIs = $Request.Body.RecipientDomainIs + $ExceptIfSentTo = $Request.Body.ExceptIfSentTo + $ExceptIfSentToMemberOf = $Request.Body.ExceptIfSentToMemberOf + $ExceptIfRecipientDomainIs = $Request.Body.ExceptIfRecipientDomainIs + + function Test-PolicyExists { + param($TenantFilter, $PolicyName) + $ExistingPolicies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-SafeLinksPolicy' -useSystemMailbox $true + return $ExistingPolicies | Where-Object { $_.Name -eq $PolicyName } + } + + function Test-RuleExists { + param($TenantFilter, $RuleName) + $ExistingRules = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-SafeLinksRule' -useSystemMailbox $true + return $ExistingRules | Where-Object { $_.Name -eq $RuleName } + } + + # Helper function to process array fields + function Process-ArrayField { + param ( + [Parameter(Mandatory = $false)] + $Field + ) + + if ($null -eq $Field) { return @() } + + # If already an array, process each item + if ($Field -is [array]) { + $result = [System.Collections.ArrayList]@() + foreach ($item in $Field) { + if ($item -is [string]) { + $result.Add($item) | Out-Null + } + elseif ($item -is [hashtable] -or $item -is [PSCustomObject]) { + # Extract value from object + if ($null -ne $item.value) { + $result.Add($item.value) | Out-Null + } + elseif ($null -ne $item.userPrincipalName) { + $result.Add($item.userPrincipalName) | Out-Null + } + elseif ($null -ne $item.id) { + $result.Add($item.id) | Out-Null + } + else { + $result.Add($item.ToString()) | Out-Null + } + } + else { + $result.Add($item.ToString()) | Out-Null + } + } + return $result.ToArray() + } + + # If it's a single object + if ($Field -is [hashtable] -or $Field -is [PSCustomObject]) { + if ($null -ne $Field.value) { return @($Field.value) } + if ($null -ne $Field.userPrincipalName) { return @($Field.userPrincipalName) } + if ($null -ne $Field.id) { return @($Field.id) } + } + + # If it's a string, return as an array with one item + if ($Field -is [string]) { + return @($Field) + } + + return @($Field) + } + + # Process all array fields + $SentTo = Process-ArrayField -Field $SentTo + $SentToMemberOf = Process-ArrayField -Field $SentToMemberOf + $RecipientDomainIs = Process-ArrayField -Field $RecipientDomainIs + $ExceptIfSentTo = Process-ArrayField -Field $ExceptIfSentTo + $ExceptIfSentToMemberOf = Process-ArrayField -Field $ExceptIfSentToMemberOf + $ExceptIfRecipientDomainIs = Process-ArrayField -Field $ExceptIfRecipientDomainIs + $DoNotRewriteUrls = Process-ArrayField -Field $DoNotRewriteUrls + + try { + # Check if policy already exists + if (Test-PolicyExists -TenantFilter $TenantFilter -PolicyName $PolicyName) { + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Policy '$PolicyName' already exists" -Sev 'Warning' + return "Policy '$PolicyName' already exists in tenant $TenantFilter" + } + + # Check if rule already exists + if (Test-RuleExists -TenantFilter $TenantFilter -RuleName $RuleName) { + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Rule '$RuleName' already exists" -Sev 'Warning' + return "Rule '$RuleName' already exists in tenant $TenantFilter" + } + + # Build command parameters for policy + $policyParams = @{ + Name = $PolicyName + } + + # Only add parameters that are explicitly provided + if ($null -ne $EnableSafeLinksForEmail) { $policyParams.Add('EnableSafeLinksForEmail', $EnableSafeLinksForEmail) } + if ($null -ne $EnableSafeLinksForTeams) { $policyParams.Add('EnableSafeLinksForTeams', $EnableSafeLinksForTeams) } + if ($null -ne $EnableSafeLinksForOffice) { $policyParams.Add('EnableSafeLinksForOffice', $EnableSafeLinksForOffice) } + if ($null -ne $TrackClicks) { $policyParams.Add('TrackClicks', $TrackClicks) } + if ($null -ne $AllowClickThrough) { $policyParams.Add('AllowClickThrough', $AllowClickThrough) } + if ($null -ne $ScanUrls) { $policyParams.Add('ScanUrls', $ScanUrls) } + if ($null -ne $EnableForInternalSenders) { $policyParams.Add('EnableForInternalSenders', $EnableForInternalSenders) } + if ($null -ne $DeliverMessageAfterScan) { $policyParams.Add('DeliverMessageAfterScan', $DeliverMessageAfterScan) } + if ($null -ne $DisableUrlRewrite) { $policyParams.Add('DisableUrlRewrite', $DisableUrlRewrite) } + if ($null -ne $DoNotRewriteUrls -and $DoNotRewriteUrls.Count -gt 0) { $policyParams.Add('DoNotRewriteUrls', $DoNotRewriteUrls) } + if ($null -ne $AdminDisplayName) { $policyParams.Add('AdminDisplayName', $AdminDisplayName) } + if ($null -ne $CustomNotificationText) { $policyParams.Add('CustomNotificationText', $CustomNotificationText) } + if ($null -ne $EnableOrganizationBranding) { $policyParams.Add('EnableOrganizationBranding', $EnableOrganizationBranding) } + + $ExoPolicyRequestParam = @{ + tenantid = $TenantFilter + cmdlet = 'New-SafeLinksPolicy' + cmdParams = $policyParams + useSystemMailbox = $true + } + + $null = New-ExoRequest @ExoPolicyRequestParam + $PolicyResult = "Successfully created new SafeLinks policy '$PolicyName'" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $PolicyResult -Sev 'Info' + + # Build command parameters for rule + $ruleParams = @{ + Name = $RuleName + SafeLinksPolicy = $PolicyName + } + + # Only add parameters that are explicitly provided + if ($null -ne $Priority) { $ruleParams.Add('Priority', $Priority) } + if ($null -ne $Comments) { $ruleParams.Add('Comments', $Comments) } + if ($null -ne $SentTo -and $SentTo.Count -gt 0) { $ruleParams.Add('SentTo', $SentTo) } + if ($null -ne $SentToMemberOf -and $SentToMemberOf.Count -gt 0) { $ruleParams.Add('SentToMemberOf', $SentToMemberOf) } + if ($null -ne $RecipientDomainIs -and $RecipientDomainIs.Count -gt 0) { $ruleParams.Add('RecipientDomainIs', $RecipientDomainIs) } + if ($null -ne $ExceptIfSentTo -and $ExceptIfSentTo.Count -gt 0) { $ruleParams.Add('ExceptIfSentTo', $ExceptIfSentTo) } + if ($null -ne $ExceptIfSentToMemberOf -and $ExceptIfSentToMemberOf.Count -gt 0) { $ruleParams.Add('ExceptIfSentToMemberOf', $ExceptIfSentToMemberOf) } + if ($null -ne $ExceptIfRecipientDomainIs -and $ExceptIfRecipientDomainIs.Count -gt 0) { $ruleParams.Add('ExceptIfRecipientDomainIs', $ExceptIfRecipientDomainIs) } + + $ExoRuleRequestParam = @{ + tenantid = $TenantFilter + cmdlet = 'New-SafeLinksRule' + cmdParams = $ruleParams + useSystemMailbox = $true + } + + $null = New-ExoRequest @ExoRuleRequestParam + + # If State is specified, enable or disable the rule + if ($null -ne $State) { + $EnableCmdlet = $State ? 'Enable-SafeLinksRule' : 'Disable-SafeLinksRule' + $EnableRequestParam = @{ + tenantid = $TenantFilter + cmdlet = $EnableCmdlet + cmdParams = @{ + Identity = $RuleName + } + useSystemMailbox = $true + } + + $null = New-ExoRequest @EnableRequestParam + } + + $RuleResult = "Successfully created new SafeLinks rule '$RuleName'" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $RuleResult -Sev 'Info' + + $Result = "Successfully created new SafeLinks policy '$PolicyName'and rule '$RuleName'" + $StatusCode = [HttpStatusCode]::OK + } + catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed creating new SafeLinks policy '$PolicyName'and rule '$RuleName'. Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev 'Error' + $StatusCode = [HttpStatusCode]::InternalServerError + } + + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{Results = $Result } + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicy.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicy.ps1 new file mode 100644 index 000000000000..a18d03c2aba1 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicy.ps1 @@ -0,0 +1,201 @@ +using namespace System.Net +Function Invoke-ListSafeLinksPolicy { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.SafeLinksPolicy.Read + .DESCRIPTION + This function is used to list the Safe Links policies in the tenant, including unmatched rules and policies. + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + $Tenantfilter = $request.Query.tenantfilter + + try { + $Policies = New-ExoRequest -tenantid $Tenantfilter -cmdlet 'Get-SafeLinksPolicy' | Select-Object -Property * -ExcludeProperty '*@odata.type' , '*@data.type' + $Rules = New-ExoRequest -tenantid $Tenantfilter -cmdlet 'Get-SafeLinksRule' | Select-Object -Property * -ExcludeProperty '*@odata.type' , '*@data.type' + $BuiltInRules = New-ExoRequest -tenantid $Tenantfilter -cmdlet 'Get-EOPProtectionPolicyRule' | Select-Object -Property * -ExcludeProperty '*@odata.type' , '*@data.type' + + # Track matched items to identify orphans + $MatchedRules = [System.Collections.Generic.HashSet[string]]::new() + $MatchedPolicies = [System.Collections.Generic.HashSet[string]]::new() + $MatchedBuiltInRules = [System.Collections.Generic.HashSet[string]]::new() + $Output = [System.Collections.Generic.List[PSCustomObject]]::new() + + # First pass: Process policies with their associated rules + foreach ($policy in $Policies) { + $policyName = $policy.Name + $MatchedPolicies.Add($policyName) | Out-Null + + # Find associated rule (single lookup per policy) + $associatedRule = $null + foreach ($rule in $Rules) { + if ($rule.SafeLinksPolicy -eq $policyName) { + $associatedRule = $rule + $MatchedRules.Add($rule.Name) | Out-Null + break + } + } + + # Find matching built-in rule (single lookup per policy) + $matchingBuiltInRule = $null + foreach ($builtInRule in $BuiltInRules) { + if ($policyName -like "$($builtInRule.Name)*") { + $matchingBuiltInRule = $builtInRule + $MatchedBuiltInRules.Add($builtInRule.Name) | Out-Null + break + } + } + + # Create output object for matched policy + $OutputItem = [PSCustomObject]@{ + # Copy all original policy properties + Name = $policy.Name + AdminDisplayName = $policy.AdminDisplayName + EnableSafeLinksForEmail = $policy.EnableSafeLinksForEmail + EnableSafeLinksForTeams = $policy.EnableSafeLinksForTeams + EnableSafeLinksForOffice = $policy.EnableSafeLinksForOffice + TrackClicks = $policy.TrackClicks + AllowClickThrough = $policy.AllowClickThrough + ScanUrls = $policy.ScanUrls + EnableForInternalSenders = $policy.EnableForInternalSenders + DeliverMessageAfterScan = $policy.DeliverMessageAfterScan + DisableUrlRewrite = $policy.DisableUrlRewrite + DoNotRewriteUrls = $policy.DoNotRewriteUrls + CustomNotificationText = $policy.CustomNotificationText + EnableOrganizationBranding = $policy.EnableOrganizationBranding + + # Calculated properties + PolicyName = $policyName + RuleName = $associatedRule.Name + Priority = if ($matchingBuiltInRule) { $matchingBuiltInRule.Priority } else { $associatedRule.Priority } + State = if ($matchingBuiltInRule) { $matchingBuiltInRule.State } else { $associatedRule.State } + SentTo = $associatedRule.SentTo + SentToMemberOf = $associatedRule.SentToMemberOf + RecipientDomainIs = $associatedRule.RecipientDomainIs + ExceptIfSentTo = $associatedRule.ExceptIfSentTo + ExceptIfSentToMemberOf = $associatedRule.ExceptIfSentToMemberOf + ExceptIfRecipientDomainIs = $associatedRule.ExceptIfRecipientDomainIs + Description = $policy.AdminDisplayName + IsBuiltIn = ($matchingBuiltInRule -ne $null) + IsValid = $policy.IsValid + ConfigurationStatus = if ($associatedRule) { "Complete" } else { "Policy Only (Missing Rule)" } + } + $Output.Add($OutputItem) + } + + # Second pass: Add unmatched rules (orphaned rules without policies) + foreach ($rule in $Rules) { + if (-not $MatchedRules.Contains($rule.Name)) { + # This rule doesn't have a matching policy + $OutputItem = [PSCustomObject]@{ + # Policy properties (null since no policy exists) + Name = $null + AdminDisplayName = $null + EnableSafeLinksForEmail = $null + EnableSafeLinksForTeams = $null + EnableSafeLinksForOffice = $null + TrackClicks = $null + AllowClickThrough = $null + ScanUrls = $null + EnableForInternalSenders = $null + DeliverMessageAfterScan = $null + DisableUrlRewrite = $null + DoNotRewriteUrls = $null + CustomNotificationText = $null + EnableOrganizationBranding = $null + + # Rule properties + PolicyName = $rule.SafeLinksPolicy + RuleName = $rule.Name + Priority = $rule.Priority + State = $rule.State + SentTo = $rule.SentTo + SentToMemberOf = $rule.SentToMemberOf + RecipientDomainIs = $rule.RecipientDomainIs + ExceptIfSentTo = $rule.ExceptIfSentTo + ExceptIfSentToMemberOf = $rule.ExceptIfSentToMemberOf + ExceptIfRecipientDomainIs = $rule.ExceptIfRecipientDomainIs + Description = $rule.Comments + IsBuiltIn = $false + ConfigurationStatus = "Rule Only (Missing Policy: $($rule.SafeLinksPolicy))" + } + $Output.Add($OutputItem) + } + } + + # Third pass: Add unmatched built-in rules + foreach ($builtInRule in $BuiltInRules) { + if (-not $MatchedBuiltInRules.Contains($builtInRule.Name)) { + # Check if this built-in rule might be SafeLinks related + if ($builtInRule.Name -like "*SafeLinks*" -or $builtInRule.Name -like "*Safe*Links*") { + $OutputItem = [PSCustomObject]@{ + # Policy properties (null since no policy exists) + Name = $null + AdminDisplayName = $null + EnableSafeLinksForEmail = $null + EnableSafeLinksForTeams = $null + EnableSafeLinksForOffice = $null + TrackClicks = $null + AllowClickThrough = $null + ScanUrls = $null + EnableForInternalSenders = $null + DeliverMessageAfterScan = $null + DisableUrlRewrite = $null + DoNotRewriteUrls = $null + CustomNotificationText = $null + EnableOrganizationBranding = $null + + # Built-in rule properties + PolicyName = $null + RuleName = $builtInRule.Name + Priority = $builtInRule.Priority + State = $builtInRule.State + SentTo = $builtInRule.SentTo + SentToMemberOf = $builtInRule.SentToMemberOf + RecipientDomainIs = $builtInRule.RecipientDomainIs + ExceptIfSentTo = $builtInRule.ExceptIfSentTo + ExceptIfSentToMemberOf = $builtInRule.ExceptIfSentToMemberOf + ExceptIfRecipientDomainIs = $builtInRule.ExceptIfRecipientDomainIs + Description = $builtInRule.Comments + IsBuiltIn = $true + ConfigurationStatus = "Built-In Rule Only (No Associated Policy)" + } + $Output.Add($OutputItem) + } + } + } + + # Sort output by ConfigurationStatus and Name for better organization + $SortedOutput = $Output.ToArray() | Sort-Object ConfigurationStatus, Name, RuleName + + # Generate summary statistics + $CompleteConfigs = ($SortedOutput | Where-Object { $_.ConfigurationStatus -eq "Complete" }).Count + $PolicyOnlyConfigs = ($SortedOutput | Where-Object { $_.ConfigurationStatus -like "*Policy Only*" }).Count + $RuleOnlyConfigs = ($SortedOutput | Where-Object { $_.ConfigurationStatus -like "*Rule Only*" }).Count + $BuiltInOnlyConfigs = ($SortedOutput | Where-Object { $_.ConfigurationStatus -like "*Built-In Rule Only*" }).Count + + if ($PolicyOnlyConfigs -gt 0 -or $RuleOnlyConfigs -gt 0) { + Write-LogMessage -headers $Headers -API $APIName -message "Found $($PolicyOnlyConfigs + $RuleOnlyConfigs) orphaned SafeLinks configurations that may need attention" -Sev 'Warning' + } + + $StatusCode = [HttpStatusCode]::OK + $FinalOutput = $SortedOutput + } + catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -headers $Headers -API $APIName -message "Error retrieving Safe Links policies: $ErrorMessage" -Sev 'Error' + $StatusCode = [HttpStatusCode]::Forbidden + $FinalOutput = $ErrorMessage + } + + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = $FinalOutput + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicyDetails.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicyDetails.ps1 new file mode 100644 index 000000000000..c0b11a9c3ad9 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicyDetails.ps1 @@ -0,0 +1,106 @@ +using namespace System.Net +function Invoke-ListSafeLinksPolicyDetails { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Exchange.SpamFilter.Read + .DESCRIPTION + This function retrieves details for a specific Safe Links policy and rule. + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + + # Interact with query parameters or the body of the request. + $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter + $PolicyName = $Request.Query.PolicyName ?? $Request.Body.PolicyName + $RuleName = $Request.Query.RuleName ?? $Request.Body.RuleName + + $Result = @{} + $LogMessages = [System.Collections.ArrayList]@() + + try { + # Get policy details if PolicyName is provided + if ($PolicyName) { + try { + $PolicyRequestParam = @{ + tenantid = $TenantFilter + cmdlet = 'Get-SafeLinksPolicy' + cmdParams = @{ + Identity = $PolicyName + } + useSystemMailbox = $true + } + $PolicyDetails = New-ExoRequest @PolicyRequestParam + $Result.Policy = $PolicyDetails + $Result.PolicyName = $PolicyDetails.Name + $LogMessages.Add("Successfully retrieved details for SafeLinks policy '$PolicyName'") | Out-Null + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Successfully retrieved details for SafeLinks policy '$PolicyName'" -Sev 'Info' + } + catch { + $ErrorMessage = Get-CippException -Exception $_ + $LogMessages.Add("Failed to retrieve details for SafeLinks policy '$PolicyName'. Error: $($ErrorMessage.NormalizedError)") | Out-Null + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to retrieve details for SafeLinks policy '$PolicyName'. Error: $($ErrorMessage.NormalizedError)" -Sev 'Warning' + $Result.PolicyError = "Failed to retrieve: $($ErrorMessage.NormalizedError)" + } + } + else { + $LogMessages.Add("No policy name provided, skipping policy retrieval") | Out-Null + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "No policy name provided, skipping policy retrieval" -Sev 'Info' + } + + # Get rule details if RuleName is provided + if ($RuleName) { + try { + $RuleRequestParam = @{ + tenantid = $TenantFilter + cmdlet = 'Get-SafeLinksRule' + cmdParams = @{ + Identity = $RuleName + } + useSystemMailbox = $true + } + $RuleDetails = New-ExoRequest @RuleRequestParam + $Result.Rule = $RuleDetails + $Result.RuleName = $RuleDetails.Name + $LogMessages.Add("Successfully retrieved details for SafeLinks rule '$RuleName'") | Out-Null + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Successfully retrieved details for SafeLinks rule '$RuleName'" -Sev 'Info' + } + catch { + $ErrorMessage = Get-CippException -Exception $_ + $LogMessages.Add("Failed to retrieve details for SafeLinks rule '$RuleName'. Error: $($ErrorMessage.NormalizedError)") | Out-Null + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to retrieve details for SafeLinks rule '$RuleName'. Error: $($ErrorMessage.NormalizedError)" -Sev 'Warning' + $Result.RuleError = "Failed to retrieve: $($ErrorMessage.NormalizedError)" + } + } + else { + $LogMessages.Add("No rule name provided, skipping rule retrieval") | Out-Null + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "No rule name provided, skipping rule retrieval" -Sev 'Info' + } + + # If no valid retrievals were performed, throw an error + if (-not ($Result.Policy -or $Result.Rule)) { + throw "No valid policy or rule details could be retrieved" + } + + # Set success status + $StatusCode = [HttpStatusCode]::OK + $Result.Message = $LogMessages -join " | " + } + catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Operation failed: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev 'Error' + $StatusCode = [HttpStatusCode]::InternalServerError + } + + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{Results = $Result } + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicyTemplateDetails.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicyTemplateDetails.ps1 new file mode 100644 index 000000000000..26f19bceb065 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicyTemplateDetails.ps1 @@ -0,0 +1,57 @@ +using namespace System.Net +Function Invoke-ListSafeLinksPolicyTemplateDetails { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + Exchange.SafeLinks.Read + .DESCRIPTION + This function retrieves details for a specific Safe Links policy template. + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + + # Get the template ID from query parameters + $ID = $Request.Query.ID ?? $Request.Body.ID + + $Result = @{} + + try { + if (-not $ID) { + throw "Template ID is required" + } + + # Get the specific template from Azure Table Storage + $Table = Get-CippTable -tablename 'templates' + $Filter = "PartitionKey eq 'SafeLinksTemplate' and RowKey eq '$ID'" + $Template = Get-CIPPAzDataTableEntity @Table -Filter $Filter + + if (-not $Template) { + throw "Template with ID '$ID' not found" + } + + # Parse the JSON data and add metadata + $TemplateData = $Template.JSON | ConvertFrom-Json + $TemplateData | Add-Member -NotePropertyName 'GUID' -NotePropertyValue $Template.RowKey -Force + + $Result = $TemplateData + $StatusCode = [HttpStatusCode]::OK + Write-LogMessage -headers $Headers -API $APIName -message "Successfully retrieved template details for ID '$ID'" -Sev 'Info' + } + catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to retrieve template details for ID '$ID'. Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Error' + $StatusCode = [HttpStatusCode]::InternalServerError + } + + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{Results = $Result } + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicyTemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicyTemplates.ps1 new file mode 100644 index 000000000000..5c4477985199 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicyTemplates.ps1 @@ -0,0 +1,39 @@ +using namespace System.Net +Function Invoke-ListSafeLinksPolicyTemplates { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + Exchange.SafeLinks.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + $Table = Get-CippTable -tablename 'templates' + $Templates = Get-ChildItem 'Config\*.SafeLinksTemplate.json' | ForEach-Object { + $Entity = @{ + JSON = "$(Get-Content $_)" + RowKey = "$($_.name)" + PartitionKey = 'SafeLinksTemplate' + GUID = "$($_.name)" + } + Add-CIPPAzDataTableEntity @Table -Entity $Entity -Force + } + #List policies + $Table = Get-CippTable -tablename 'templates' + $Filter = "PartitionKey eq 'SafeLinksTemplate'" + $Templates = (Get-CIPPAzDataTableEntity @Table -Filter $Filter) | ForEach-Object { + $GUID = $_.RowKey + $data = $_.JSON | ConvertFrom-Json + $data | Add-Member -NotePropertyName 'GUID' -NotePropertyValue $GUID + $data + } + if ($Request.query.ID) { $Templates = $Templates | Where-Object -Property RowKey -EQ $Request.query.id } + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = @($Templates) + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-RemoveSafeLinksPolicyTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-RemoveSafeLinksPolicyTemplate.ps1 new file mode 100644 index 000000000000..676b72e4b17e --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-RemoveSafeLinksPolicyTemplate.ps1 @@ -0,0 +1,35 @@ +using namespace System.Net + +Function Invoke-RemoveSafeLinksPolicyTemplate { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + Exchange.SafeLinks.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + $APIName = $Request.Params.CIPPEndpoint + $User = $Request.Headers + Write-LogMessage -Headers $User -API $APINAME -message 'Accessed this API' -Sev 'Debug' + $ID = $request.query.ID ?? $request.body.ID + try { + $Table = Get-CippTable -tablename 'templates' + $Filter = "PartitionKey eq 'SafeLinksTemplate' and RowKey eq '$id'" + $ClearRow = Get-CIPPAzDataTableEntity @Table -Filter $Filter -Property PartitionKey, RowKey + Remove-AzDataTableEntity -Force @Table -Entity $ClearRow + $Result = "Removed SafeLinks Policy Template with ID $ID." + Write-LogMessage -Headers $User -API $APINAME -message $Result -Sev 'Info' + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to remove SafeLinks Policy template with ID $ID. Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -Headers $User -API $APINAME -message $Result -Sev 'Error' -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::Forbidden + } + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{ Results = $Result } + }) +} diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksTemplatePolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksTemplatePolicy.ps1 new file mode 100644 index 000000000000..e630e12a900b --- /dev/null +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksTemplatePolicy.ps1 @@ -0,0 +1,489 @@ +function Invoke-CIPPStandardSafeLinksTemplatePolicy { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) SafeLinksTemplatePolicy + .SYNOPSIS + (Label) SafeLinks Policy Template + .DESCRIPTION + (Helptext) This applies selected SafeLinks policy templates to the tenant, creating or updating as needed + (DocsDescription) This applies selected SafeLinks policy templates to the tenant, creating or updating as needed + .NOTES + CAT + Defender Standards + TAG + "CIS" + "mdo_safelinksforemail" + "mdo_safelinksforOfficeApps" + ADDEDCOMPONENT + {"type":"autoComplete","multiple":true,"name":"standards.SafeLinksTemplatePolicy.TemplateIds","label":"SafeLinks Templates","loadingMessage":"Loading templates...","api":{"url":"/api/ListSafeLinksPolicyTemplates","labelField":"name","valueField":"GUID","queryKey":"ListSafeLinksPolicyTemplates"}} + IMPACT + Low Impact + ADDEDDATE + 2025-04-29 + POWERSHELLEQUIVALENT + New-SafeLinksPolicy, Set-SafeLinksPolicy, New-SafeLinksRule, Set-SafeLinksRule + RECOMMENDEDBY + "CIS" + .LINK + https://docs.cipp.app/user-documentation/tenant/standards/list-standards/defender-standards#low-impact + #> + + param($Tenant, $Settings) + + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Processing SafeLinks template with settings: $($Settings | ConvertTo-Json -Compress)" -sev Debug + + # Verify tenant has necessary license + if (-not (Test-MDOLicense -Tenant $Tenant -Settings $Settings)) { + return + } + + # Normalize template list property + $TemplateList = Get-NormalizedTemplateList -Settings $Settings + if (-not $TemplateList) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "No templates selected for SafeLinks policy deployment" -sev Error + return + } + + # Handle different modes + switch ($true) { + ($Settings.remediate -eq $true) { + Invoke-SafeLinksRemediation -Tenant $Tenant -TemplateList $TemplateList -Settings $Settings + } + ($Settings.alert -eq $true) { + Invoke-SafeLinksAlert -Tenant $Tenant -TemplateList $TemplateList -Settings $Settings + } + ($Settings.report -eq $true) { + Invoke-SafeLinksReport -Tenant $Tenant -TemplateList $TemplateList -Settings $Settings + } + } +} + +function Test-MDOLicense { + param($Tenant, $Settings) + + $ServicePlans = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/subscribedSkus?$select=servicePlans' -tenantid $Tenant + $ServicePlans = $ServicePlans.servicePlans.servicePlanName + $MDOLicensed = $ServicePlans -contains 'ATP_ENTERPRISE' + + if (-not $MDOLicensed) { + $Message = 'Tenant does not have Microsoft Defender for Office 365 license' + + if ($Settings.remediate -eq $true) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to apply SafeLinks templates: $Message" -sev Error + } + + if ($Settings.alert -eq $true) { + Write-StandardsAlert -message "SafeLinks templates could not be applied: $Message" -object $MDOLicensed -tenant $Tenant -standardName 'SafeLinksTemplatePolicy' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $Tenant -message "SafeLinks templates could not be applied: $Message" -sev Info + } + + if ($Settings.report -eq $true) { + Add-CIPPBPAField -FieldName 'SafeLinksTemplatePolicy' -FieldValue $false -StoreAs bool -Tenant $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.SafeLinksTemplatePolicy' -FieldValue $false -Tenant $Tenant + } + + return $false + } + + return $true +} + +function Get-NormalizedTemplateList { + param($Settings) + + if ($Settings.'standards.SafeLinksTemplatePolicy.TemplateIds') { + return $Settings.'standards.SafeLinksTemplatePolicy.TemplateIds' + } + elseif ($Settings.TemplateIds) { + return $Settings.TemplateIds + } + + return $null +} + +function Get-SafeLinksTemplateFromStorage { + param($TemplateId) + + $Table = Get-CippTable -tablename 'templates' + $Filter = "PartitionKey eq 'SafeLinksTemplate' and RowKey eq '$TemplateId'" + $Template = Get-CIPPAzDataTableEntity @Table -Filter $Filter + + if (-not $Template) { + throw "Template with ID $TemplateId not found" + } + + return $Template.JSON | ConvertFrom-Json -ErrorAction Stop +} + +function ConvertTo-SafeArray { + param($Field) + + if ($null -eq $Field) { return @() } + + $ResultList = [System.Collections.Generic.List[string]]::new() + + if ($Field -is [array]) { + foreach ($item in $Field) { + if ($item -is [string]) { + $ResultList.Add($item) + } + elseif ($item.value) { + $ResultList.Add($item.value) + } + elseif ($item.userPrincipalName) { + $ResultList.Add($item.userPrincipalName) + } + elseif ($item.id) { + $ResultList.Add($item.id) + } + else { + $ResultList.Add($item.ToString()) + } + } + return $ResultList.ToArray() + } + + if ($Field -is [hashtable] -or $Field -is [PSCustomObject]) { + if ($Field.value) { + $ResultList.Add($Field.value) + return $ResultList.ToArray() + } + if ($Field.userPrincipalName) { + $ResultList.Add($Field.userPrincipalName) + return $ResultList.ToArray() + } + if ($Field.id) { + $ResultList.Add($Field.id) + return $ResultList.ToArray() + } + } + + if ($Field -is [string]) { + $ResultList.Add($Field) + return $ResultList.ToArray() + } + + $ResultList.Add($Field.ToString()) + return $ResultList.ToArray() +} + +function Get-ExistingSafeLinksObjects { + param($Tenant, $PolicyName, $RuleName) + + $PolicyExists = $null + $RuleExists = $null + + try { + $ExistingPolicies = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-SafeLinksPolicy' -useSystemMailbox $true + $PolicyExists = $ExistingPolicies | Where-Object { $_.Name -eq $PolicyName } + } + catch { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to retrieve existing policies: $($_.Exception.Message)" -sev Warning + } + + try { + $ExistingRules = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-SafeLinksRule' -useSystemMailbox $true + $RuleExists = $ExistingRules | Where-Object { $_.Name -eq $RuleName } + } + catch { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to retrieve existing rules: $($_.Exception.Message)" -sev Warning + } + + return @{ + PolicyExists = $PolicyExists + RuleExists = $RuleExists + } +} + +function New-SafeLinksPolicyParameters { + param($Template) + + $PolicyMappings = @{ + 'EnableSafeLinksForEmail' = 'EnableSafeLinksForEmail' + 'EnableSafeLinksForTeams' = 'EnableSafeLinksForTeams' + 'EnableSafeLinksForOffice' = 'EnableSafeLinksForOffice' + 'TrackClicks' = 'TrackClicks' + 'AllowClickThrough' = 'AllowClickThrough' + 'ScanUrls' = 'ScanUrls' + 'EnableForInternalSenders' = 'EnableForInternalSenders' + 'DeliverMessageAfterScan' = 'DeliverMessageAfterScan' + 'DisableUrlRewrite' = 'DisableUrlRewrite' + 'AdminDisplayName' = 'AdminDisplayName' + 'CustomNotificationText' = 'CustomNotificationText' + 'EnableOrganizationBranding' = 'EnableOrganizationBranding' + } + + $PolicyParams = @{} + + foreach ($templateKey in $PolicyMappings.Keys) { + if ($null -ne $Template.$templateKey) { + $PolicyParams[$PolicyMappings[$templateKey]] = $Template.$templateKey + } + } + + $DoNotRewriteUrls = ConvertTo-SafeArray -Field $Template.DoNotRewriteUrls + if ($DoNotRewriteUrls.Count -gt 0) { + $PolicyParams['DoNotRewriteUrls'] = $DoNotRewriteUrls + } + + return $PolicyParams +} + +function New-SafeLinksRuleParameters { + param($Template) + + $RuleParams = @{} + + # Basic rule parameters + if ($null -ne $Template.Priority) { $RuleParams['Priority'] = $Template.Priority } + if ($null -ne $Template.Description) { $RuleParams['Comments'] = $Template.Description } + if ($null -ne $Template.TemplateDescription) { $RuleParams['Comments'] = $Template.TemplateDescription } + + # Array-based rule parameters + $ArrayMappings = @{ + 'SentTo' = ConvertTo-SafeArray -Field $Template.SentTo + 'SentToMemberOf' = ConvertTo-SafeArray -Field $Template.SentToMemberOf + 'RecipientDomainIs' = ConvertTo-SafeArray -Field $Template.RecipientDomainIs + 'ExceptIfSentTo' = ConvertTo-SafeArray -Field $Template.ExceptIfSentTo + 'ExceptIfSentToMemberOf' = ConvertTo-SafeArray -Field $Template.ExceptIfSentToMemberOf + 'ExceptIfRecipientDomainIs' = ConvertTo-SafeArray -Field $Template.ExceptIfRecipientDomainIs + } + + foreach ($paramName in $ArrayMappings.Keys) { + if ($ArrayMappings[$paramName].Count -gt 0) { + $RuleParams[$paramName] = $ArrayMappings[$paramName] + } + } + + return $RuleParams +} + +function Set-SafeLinksRuleState { + param($Tenant, $RuleName, $State) + + if ($null -eq $State) { return } + + $IsEnabled = switch ($State) { + "Enabled" { $true } + "Disabled" { $false } + $true { $true } + $false { $false } + default { $null } + } + + if ($null -ne $IsEnabled) { + $Cmdlet = $IsEnabled ? 'Enable-SafeLinksRule' : 'Disable-SafeLinksRule' + $null = New-ExoRequest -tenantid $Tenant -cmdlet $Cmdlet -cmdParams @{ Identity = $RuleName } -useSystemMailbox $true + return $IsEnabled ? "enabled" : "disabled" + } + + return $null +} + +function Invoke-SafeLinksRemediation { + param($Tenant, $TemplateList, $Settings) + + $OverallSuccess = $true + $TemplateResults = @{} + + foreach ($TemplateItem in $TemplateList) { + $TemplateId = $TemplateItem.value + + try { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Processing SafeLinks template with ID: $TemplateId" -sev Info + + # Get template from storage + $Template = Get-SafeLinksTemplateFromStorage -TemplateId $TemplateId + + $PolicyName = $Template.PolicyName ?? $Template.Name + $RuleName = $Template.RuleName ?? "$($PolicyName)_Rule" + + # Check existing objects + $ExistingObjects = Get-ExistingSafeLinksObjects -Tenant $Tenant -PolicyName $PolicyName -RuleName $RuleName + + $ActionsTaken = [System.Collections.Generic.List[string]]::new() + + # Process Policy + $PolicyParams = New-SafeLinksPolicyParameters -Template $Template + + if ($ExistingObjects.PolicyExists) { + # Update existing policy to keep it in line + $PolicyParams['Identity'] = $PolicyName + $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-SafeLinksPolicy' -cmdParams $PolicyParams -useSystemMailbox $true + $ActionsTaken.Add("Updated SafeLinks policy '$PolicyName'") + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Updated SafeLinks policy '$PolicyName'" -sev Info + } + else { + # Create new policy + $PolicyParams['Name'] = $PolicyName + $null = New-ExoRequest -tenantid $Tenant -cmdlet 'New-SafeLinksPolicy' -cmdParams $PolicyParams -useSystemMailbox $true + $ActionsTaken.Add("Created new SafeLinks policy '$PolicyName'") + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Created new SafeLinks policy '$PolicyName'" -sev Info + } + + # Process Rule + $RuleParams = New-SafeLinksRuleParameters -Template $Template + + if ($ExistingObjects.RuleExists) { + # Update existing rule to keep it in line + $RuleParams['Identity'] = $RuleName + $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-SafeLinksRule' -cmdParams $RuleParams -useSystemMailbox $true + $ActionsTaken.Add("Updated SafeLinks rule '$RuleName'") + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Updated SafeLinks rule '$RuleName'" -sev Info + } + else { + # Create new rule + $RuleParams['Name'] = $RuleName + $RuleParams['SafeLinksPolicy'] = $PolicyName + $null = New-ExoRequest -tenantid $Tenant -cmdlet 'New-SafeLinksRule' -cmdParams $RuleParams -useSystemMailbox $true + $ActionsTaken.Add("Created new SafeLinks rule '$RuleName'") + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Created new SafeLinks rule '$RuleName'" -sev Info + } + + # Set rule state + $StateResult = Set-SafeLinksRuleState -Tenant $Tenant -RuleName $RuleName -State $Template.State + if ($StateResult) { + $ActionsTaken.Add("Rule $StateResult") + Write-LogMessage -API 'Standards' -tenant $Tenant -message "SafeLinks rule '$RuleName' $StateResult" -sev Info + } + + $TemplateResults[$TemplateId] = @{ + Success = $true + ActionsTaken = $ActionsTaken.ToArray() + TemplateName = $Template.TemplateName ?? $Template.Name + PolicyName = $PolicyName + RuleName = $RuleName + } + + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully applied SafeLinks template '$($Template.TemplateName ?? $Template.Name)'" -sev Info + } + catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + $TemplateResults[$TemplateId] = @{ + Success = $false + Message = $ErrorMessage + TemplateName = $Template.TemplateName ?? $Template.Name ?? "Unknown" + } + $OverallSuccess = $false + + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to apply SafeLinks template ID $TemplateId : $ErrorMessage" -sev Error + } + } + + # Report overall results + if ($OverallSuccess) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully applied all SafeLinks templates" -sev Info + } + else { + $SuccessCount = ($TemplateResults.Values | Where-Object { $_.Success -eq $true }).Count + $TotalCount = $TemplateList.Count + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Applied $SuccessCount out of $TotalCount SafeLinks templates" -sev Info + } +} + +function Invoke-SafeLinksAlert { + param($Tenant, $TemplateList, $Settings) + + $AllTemplatesApplied = $true + $AlertMessages = [System.Collections.Generic.List[string]]::new() + + foreach ($TemplateItem in $TemplateList) { + $TemplateId = $TemplateItem.value + + try { + $Template = Get-SafeLinksTemplateFromStorage -TemplateId $TemplateId + $PolicyName = $Template.PolicyName ?? $Template.Name + $RuleName = $Template.RuleName ?? "$($PolicyName)_Rule" + + $ExistingObjects = Get-ExistingSafeLinksObjects -Tenant $Tenant -PolicyName $PolicyName -RuleName $RuleName + + if (-not $ExistingObjects.PolicyExists -or -not $ExistingObjects.RuleExists) { + $AllTemplatesApplied = $false + $Status = "SafeLinks template '$($Template.TemplateName ?? $Template.Name)' is not applied" + + if (-not $ExistingObjects.PolicyExists) { + $Status = "$Status - policy '$PolicyName' does not exist" + } + + if (-not $ExistingObjects.RuleExists) { + $Status = "$Status - rule '$RuleName' does not exist" + } + + $AlertMessages.Add($Status) + } + } + catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + $AlertMessages.Add("Failed to check template with ID $TemplateId : $ErrorMessage") + $AllTemplatesApplied = $false + } + } + + if ($AllTemplatesApplied) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "All SafeLinks templates are correctly applied" -sev Info + } + else { + $AlertMessage = "One or more SafeLinks templates are not correctly applied: " + ($AlertMessages.ToArray() -join " | ") + Write-StandardsAlert -message $AlertMessage -object @{ + Templates = $TemplateList + Issues = $AlertMessages.ToArray() + } -tenant $Tenant -standardName 'SafeLinksTemplatePolicy' -standardId $Settings.standardId + + Write-LogMessage -API 'Standards' -tenant $Tenant -message $AlertMessage -sev Info + } +} + +function Invoke-SafeLinksReport { + param($Tenant, $TemplateList, $Settings) + + $AllTemplatesApplied = $true + $ReportResults = @{} + + foreach ($TemplateItem in $TemplateList) { + $TemplateId = $TemplateItem.value + + try { + $Template = Get-SafeLinksTemplateFromStorage -TemplateId $TemplateId + $PolicyName = $Template.PolicyName ?? $Template.Name + $RuleName = $Template.RuleName ?? "$($PolicyName)_Rule" + + $ExistingObjects = Get-ExistingSafeLinksObjects -Tenant $Tenant -PolicyName $PolicyName -RuleName $RuleName + + $ReportResults[$TemplateId] = @{ + Success = ($ExistingObjects.PolicyExists -and $ExistingObjects.RuleExists) + TemplateName = $Template.TemplateName ?? $Template.Name + PolicyName = $PolicyName + RuleName = $RuleName + PolicyExists = [bool]$ExistingObjects.PolicyExists + RuleExists = [bool]$ExistingObjects.RuleExists + } + + if (-not $ExistingObjects.PolicyExists -or -not $ExistingObjects.RuleExists) { + $AllTemplatesApplied = $false + } + } + catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + $ReportResults[$TemplateId] = @{ + Success = $false + Message = $ErrorMessage + } + $AllTemplatesApplied = $false + } + } + + Add-CIPPBPAField -FieldName 'SafeLinksTemplatePolicy' -FieldValue $AllTemplatesApplied -StoreAs bool -Tenant $Tenant + + if ($AllTemplatesApplied) { + Set-CIPPStandardsCompareField -FieldName 'standards.SafeLinksTemplatePolicy' -FieldValue $true -Tenant $Tenant + } + else { + Set-CIPPStandardsCompareField -FieldName 'standards.SafeLinksTemplatePolicy' -FieldValue @{ + TemplateResults = $ReportResults + ProcessedTemplates = $TemplateList.Count + SuccessfulTemplates = ($ReportResults.Values | Where-Object { $_.Success -eq $true }).Count + } -Tenant $Tenant + } +} diff --git a/openapi.json b/openapi.json index 7fb2383fa0cc..19763a14267d 100644 --- a/openapi.json +++ b/openapi.json @@ -8898,6 +8898,680 @@ } } }, + "/ExecDeleteSafeLinksPolicy": { + "get": { + "description": "ExecDeleteSafeLinksPolicy", + "summary": "ExecDeleteSafeLinksPolicy", + "tags": [ + "GET" + ], + "parameters": [ + { + "required": true, + "schema": { + "type": "string" + }, + "name": "tenantFilter", + "in": "query" + }, + { + "required": true, + "schema": { + "type": "string" + }, + "name": "RuleName", + "in": "query" + }, + { + "required": true, + "schema": { + "type": "string" + }, + "name": "PolicyName", + "in": "query" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": {}, + "type": "object" + } + } + }, + "description": "Successful operation" + } + } + } + }, + "/EditSafeLinksPolicy": { + "post": { + "description": "EditSafeLinksPolicy", + "summary": "EditSafeLinksPolicy", + "tags": [ + "POST" + ], + "parameters": [ + { + "required": true, + "schema": { + "type": "string" + }, + "name": "tenantFilter", + "in": "query" + }, + { + "required": true, + "schema": { + "type": "string" + }, + "name": "PolicyName", + "in": "query" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Name": { + "type": "string" + }, + "EnableSafeLinksForEmail": { + "type": "boolean" + }, + "EnableSafeLinksForTeams": { + "type": "boolean" + }, + "EnableSafeLinksForOffice": { + "type": "boolean" + }, + "TrackClicks": { + "type": "boolean" + }, + "AllowClickThrough": { + "type": "boolean" + }, + "ScanUrls": { + "type": "boolean" + }, + "EnableForInternalSenders": { + "type": "boolean" + }, + "DeliverMessageAfterScan": { + "type": "boolean" + }, + "DisableUrlRewrite": { + "type": "boolean" + }, + "DoNotRewriteUrls": { + "type": "array", + "items": { + "type": "string" + } + }, + "AdminDisplayName": { + "type": "string" + }, + "CustomNotificationText": { + "type": "string" + }, + "EnableOrganizationBranding": { + "type": "boolean" + }, + "Priority": { + "type": "integer" + }, + "Comments": { + "type": "string" + }, + "Enabled": { + "type": "boolean" + }, + "SentTo": { + "type": "array", + "items": { + "type": "string" + } + }, + "ExceptIfSentTo": { + "type": "array", + "items": { + "type": "string" + } + }, + "SentToMemberOf": { + "type": "array", + "items": { + "type": "string" + } + }, + "ExceptIfSentToMemberOf": { + "type": "array", + "items": { + "type": "string" + } + }, + "RecipientDomainIs": { + "type": "array", + "items": { + "type": "string" + } + }, + "ExceptIfRecipientDomainIs": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Results": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + "description": "Successful operation" + } + } + } + }, + "/ListSafeLinksPolicyDetails": { + "get": { + "description": "ListSafeLinksPolicyDetails", + "summary": "ListSafeLinksPolicyDetails", + "tags": [ + "GET" + ], + "parameters": [ + { + "required": true, + "schema": { + "type": "string" + }, + "name": "tenantFilter", + "in": "query" + }, + { + "required": true, + "schema": { + "type": "string" + }, + "name": "PolicyName", + "in": "query" + }, + { + "required": false, + "schema": { + "type": "string" + }, + "name": "RuleName", + "in": "query" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Results": { + "type": "object", + "properties": { + "Policy": { + "type": "object", + "properties": { + "Name": { + "type": "string" + }, + "EnableSafeLinksForEmail": { + "type": "boolean" + }, + "EnableSafeLinksForTeams": { + "type": "boolean" + }, + "EnableSafeLinksForOffice": { + "type": "boolean" + }, + "TrackClicks": { + "type": "boolean" + }, + "AllowClickThrough": { + "type": "boolean" + }, + "ScanUrls": { + "type": "boolean" + }, + "EnableForInternalSenders": { + "type": "boolean" + }, + "DeliverMessageAfterScan": { + "type": "boolean" + }, + "DisableUrlRewrite": { + "type": "boolean" + }, + "DoNotRewriteUrls": { + "type": "array", + "items": { + "type": "string" + } + }, + "AdminDisplayName": { + "type": "string" + }, + "CustomNotificationText": { + "type": "string" + }, + "EnableOrganizationBranding": { + "type": "boolean" + } + } + }, + "Rule": { + "type": "object", + "properties": { + "Name": { + "type": "string" + }, + "Priority": { + "type": "integer" + }, + "Comments": { + "type": "string" + }, + "State": { + "type": "string" + }, + "SentTo": { + "type": "array", + "items": { + "type": "string" + } + }, + "ExceptIfSentTo": { + "type": "array", + "items": { + "type": "string" + } + }, + "SentToMemberOf": { + "type": "array", + "items": { + "type": "string" + } + }, + "ExceptIfSentToMemberOf": { + "type": "array", + "items": { + "type": "string" + } + }, + "RecipientDomainIs": { + "type": "array", + "items": { + "type": "string" + } + }, + "ExceptIfRecipientDomainIs": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "Message": { + "type": "string" + } + } + } + } + } + } + }, + "description": "Successful operation" + } + } + } + }, + "/ExecNewSafeLinksPolicy": { + "post": { + "description": "Create a new SafeLinks policy and associated rule", + "summary": "Create SafeLinks Policy and Rule Configuration", + "tags": [ + "POST" + ], + "parameters": [ + { + "required": true, + "schema": { + "type": "string" + }, + "name": "tenantFilter", + "in": "query" + }, + { + "required": true, + "schema": { + "type": "string" + }, + "name": "Name", + "in": "query" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "EnableSafeLinksForEmail": { + "type": "boolean", + "description": "Enable Safe Links protection for email messages" + }, + "EnableSafeLinksForTeams": { + "type": "boolean", + "description": "Enable Safe Links protection for Teams messages" + }, + "EnableSafeLinksForOffice": { + "type": "boolean", + "description": "Enable Safe Links protection for Office applications" + }, + "TrackClicks": { + "type": "boolean", + "description": "Track user clicks related to Safe Links protection" + }, + "AllowClickThrough": { + "type": "boolean", + "description": "Allow users to click through to the original URL" + }, + "ScanUrls": { + "type": "boolean", + "description": "Enable real-time scanning of URLs" + }, + "EnableForInternalSenders": { + "type": "boolean", + "description": "Enable Safe Links for messages sent between internal senders" + }, + "DeliverMessageAfterScan": { + "type": "boolean", + "description": "Wait until URL scanning is complete before delivering the message" + }, + "DisableUrlRewrite": { + "type": "boolean", + "description": "Disable the rewriting of URLs in messages" + }, + "DoNotRewriteUrls": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of URLs that will not be rewritten by Safe Links" + }, + "AdminDisplayName": { + "type": "string", + "description": "Display name for the policy in the admin interface" + }, + "CustomNotificationText": { + "type": "string", + "description": "Custom text for the notification when a link is blocked" + }, + "EnableOrganizationBranding": { + "type": "boolean", + "description": "Enable organization branding on warning pages" + }, + "Priority": { + "type": "integer", + "description": "Priority of the rule (lower numbers = higher priority)" + }, + "Comments": { + "type": "string", + "description": "Administrative comments for the rule" + }, + "Enabled": { + "type": "boolean", + "description": "Whether the rule is enabled or disabled" + }, + "SentTo": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Apply the rule if the recipient is any of these email addresses" + }, + "SentToMemberOf": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Apply the rule if the recipient is a member of any of these groups" + }, + "RecipientDomainIs": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Apply the rule if the recipient's domain matches any of these domains" + }, + "ExceptIfSentTo": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Do not apply the rule if the recipient is any of these email addresses" + }, + "ExceptIfSentToMemberOf": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Do not apply the rule if the recipient is a member of any of these groups" + }, + "ExceptIfRecipientDomainIs": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Do not apply the rule if the recipient's domain matches any of these domains" + } + } + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Results": { + "type": "string", + "description": "Result message from the operation" + } + } + } + } + }, + "description": "Successfully created SafeLinks policy and rule" + }, + "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" + } + } + } + } + } + } + } + } + }, + "/ListSafeLinksPolicyTemplates": { + "get": { + "description": "List SafeLinks Policy Templates", + "summary": "List SafeLinks Policy Templates", + "tags": [ + "GET" + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": {} + } + } + } + }, + "description": "Successful operation" + } + } + } + }, + "/RemoveSafeLinksPolicyTemplate": { + "get": { + "description": "Remove SafeLinks Policy Template", + "summary": "Remove SafeLinks Policy Template", + "tags": [ + "GET" + ], + "parameters": [ + { + "required": true, + "schema": { + "type": "string" + }, + "name": "id", + "in": "query" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": {}, + "type": "object" + } + } + }, + "description": "Successful operation" + } + } + } + }, + "/AddSafeLinksPolicyTemplate": { + "post": { + "description": "Add SafeLinks Policy Template", + "summary": "Add SafeLinks Policy Template", + "tags": [ + "POST" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {} + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": {}, + "type": "object" + } + } + }, + "description": "Successful operation" + } + } + } + }, + "/AddSafeLinksPolicyFromTemplate": { + "post": { + "description": "Deploy SafeLinks Policy From Template", + "summary": "Deploy SafeLinks Policy From Template", + "tags": [ + "POST" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {} + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": {}, + "type": "object" + } + } + }, + "description": "Successful operation" + } + } + } + }, "openapi": "3.1.0", "servers": [ { From 0f72951d0b3da2aaec0753768db47266393f2f52 Mon Sep 17 00:00:00 2001 From: John Martin Askevold <50556267+StoricU@users.noreply.github.com> Date: Wed, 4 Jun 2025 09:11:36 +0200 Subject: [PATCH 032/160] Update Invoke-CIPPStandardSendReceiveLimitTenant.ps1 Handle "Unlimited" values in MaxSendSize and MaxReceiveSize to avoid conversion errors when comparing mailbox plan limits --- ...oke-CIPPStandardSendReceiveLimitTenant.ps1 | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendReceiveLimitTenant.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendReceiveLimitTenant.ps1 index 344f83bc00bc..e9e3daa4a36b 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendReceiveLimitTenant.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendReceiveLimitTenant.ps1 @@ -37,20 +37,28 @@ function Invoke-CIPPStandardSendReceiveLimitTenant { return } - # Input validation if ([Int32]$Settings.ReceiveLimit -lt 1 -or [Int32]$Settings.ReceiveLimit -gt 150) { Write-LogMessage -API 'Standards' -tenant $tenant -message 'SendReceiveLimitTenant: Invalid ReceiveLimit parameter set' -sev Error return } - $AllMailBoxPlans = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-MailboxPlan' | Select-Object DisplayName, MaxSendSize, MaxReceiveSize, GUID - $MaxSendSize = [int64]"$($Settings.SendLimit)MB" - $MaxReceiveSize = [int64]"$($Settings.ReceiveLimit)MB" + $MaxSendSize = $Settings.SendLimit * 1MB + $MaxReceiveSize = $Settings.ReceiveLimit * 1MB $NotSetCorrectly = foreach ($MailboxPlan in $AllMailBoxPlans) { - $PlanMaxSendSize = [int64]($MailboxPlan.MaxSendSize -replace '.*\(([\d,]+).*', '$1' -replace ',', '') - $PlanMaxReceiveSize = [int64]($MailboxPlan.MaxReceiveSize -replace '.*\(([\d,]+).*', '$1' -replace ',', '') + if ($MailboxPlan.MaxSendSize -eq 'Unlimited') { + $PlanMaxSendSize = [int64]::MaxValue + } else { + $PlanMaxSendSize = [int64]($MailboxPlan.MaxSendSize -replace '.*\(([\d,]+).*', '$1' -replace ',', '') + } + + if ($MailboxPlan.MaxReceiveSize -eq 'Unlimited') { + $PlanMaxReceiveSize = [int64]::MaxValue + } else { + $PlanMaxReceiveSize = [int64]($MailboxPlan.MaxReceiveSize -replace '.*\(([\d,]+).*', '$1' -replace ',', '') + } + if ($PlanMaxSendSize -ne $MaxSendSize -or $PlanMaxReceiveSize -ne $MaxReceiveSize) { $MailboxPlan } @@ -76,7 +84,6 @@ function Invoke-CIPPStandardSendReceiveLimitTenant { } if ($Settings.alert -eq $true) { - if ($NotSetCorrectly.Count -eq 0) { Write-LogMessage -API 'Standards' -tenant $tenant -message "The tenant send($($Settings.SendLimit)MB) and receive($($Settings.ReceiveLimit)MB) limits are set correctly" -sev Info } else { From 09cffff620964fbf6df350b8f7219e090d414cc7 Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 5 Jun 2025 23:51:08 +0800 Subject: [PATCH 033/160] Update Get-CIPPAlertInactiveLicensedUsers.ps1 --- .../Get-CIPPAlertInactiveLicensedUsers.ps1 | 54 +++++++++++++++---- 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveLicensedUsers.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveLicensedUsers.ps1 index c8907e481339..ca3dc7022fdf 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveLicensedUsers.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveLicensedUsers.ps1 @@ -8,28 +8,62 @@ function Get-CIPPAlertInactiveLicensedUsers { [Parameter(Mandatory = $false)] [Alias('input')] $InputValue, + [Parameter(Mandatory = $false)] #future use + [switch]$SkipNeverSignedIn, #future use $TenantFilter ) try { try { + $Lookup = (Get-Date).AddDays(-90).ToUniversalTime() - $Lookup = (Get-Date).AddDays(-90).ToUniversalTime().ToString('o') - $GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users?`$filter=(signInActivity/lastNonInteractiveSignInDateTime le $Lookup)&`$select=id,UserPrincipalName,signInActivity,mail,userType,accountEnabled,assignedLicenses" -scope 'https://graph.microsoft.com/.default' -tenantid $TenantFilter | - Where-Object { $null -ne $_.assignedLicenses.skuId } + # Build base filter - cannot filter assignedLicenses server-side + $BaseFilter = if ($InputValue -eq $true) { "accountEnabled eq true" } else { "" } + + $Uri = if ($BaseFilter) { + "https://graph.microsoft.com/beta/users?`$filter=$BaseFilter&`$select=id,UserPrincipalName,signInActivity,mail,userType,accountEnabled,assignedLicenses" + } else { + "https://graph.microsoft.com/beta/users?`$select=id,UserPrincipalName,signInActivity,mail,userType,accountEnabled,assignedLicenses" + } + + $GraphRequest = New-GraphGetRequest -uri $Uri -scope 'https://graph.microsoft.com/.default' -tenantid $TenantFilter | + Where-Object { $null -ne $_.assignedLicenses -and $_.assignedLicenses.Count -gt 0 } - # true = only active users - if ($InputValue -eq $true) { $GraphRequest = $GraphRequest | Where-Object { $_.accountEnabled -eq $true } } $AlertData = foreach ($user in $GraphRequest) { - $Message = 'User {0} has been inactive for 90 days, but still has a license assigned.' -f $user.UserPrincipalName - $user | Select-Object -Property UserPrincipalName, signInActivity, @{Name = 'Message'; Expression = { $Message } } + $lastInteractive = $user.signInActivity.lastSignInDateTime + $lastNonInteractive = $user.signInActivity.lastNonInteractiveSignInDateTime - } - Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData + # Find most recent sign-in + $lastSignIn = $null + if ($lastInteractive -and $lastNonInteractive) { + $lastSignIn = if ([DateTime]$lastInteractive -gt [DateTime]$lastNonInteractive) { $lastInteractive } else { $lastNonInteractive } + } elseif ($lastInteractive) { + $lastSignIn = $lastInteractive + } elseif ($lastNonInteractive) { + $lastSignIn = $lastNonInteractive + } - } catch {} + # Check if inactive + $isInactive = (-not $lastSignIn) -or ([DateTime]$lastSignIn -le $Lookup) + # Apply SkipNeverSignedIn filter #future use + if ($SkipNeverSignedIn -and -not $lastSignIn) { continue } + # Only process inactive users + if ($isInactive) { + if (-not $lastSignIn) { + $Message = 'User {0} has never signed in but still has a license assigned.' -f $user.UserPrincipalName + } else { + $daysSinceSignIn = [Math]::Round(((Get-Date) - [DateTime]$lastSignIn).TotalDays) + $Message = 'User {0} has been inactive for {1} days but still has a license assigned. Last sign-in: {2}' -f $user.UserPrincipalName, $daysSinceSignIn, $lastSignIn + } + + $user | Select-Object -Property UserPrincipalName, signInActivity, @{Name = 'Message'; Expression = { $Message } } + } + } + + Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData + } catch {} } catch { Write-AlertMessage -tenant $($TenantFilter) -message "Failed to check inactive users with licenses for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" } From f05c72ff506712b18eb3b44dcc1532256f5fda30 Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 5 Jun 2025 23:40:58 +0800 Subject: [PATCH 034/160] invert logic for never signed in accounts, skip be default --- .../Alerts/Get-CIPPAlertInactiveLicensedUsers.ps1 | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveLicensedUsers.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveLicensedUsers.ps1 index ca3dc7022fdf..2a0fb9ff869e 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveLicensedUsers.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveLicensedUsers.ps1 @@ -8,8 +8,8 @@ function Get-CIPPAlertInactiveLicensedUsers { [Parameter(Mandatory = $false)] [Alias('input')] $InputValue, - [Parameter(Mandatory = $false)] #future use - [switch]$SkipNeverSignedIn, #future use + [Parameter(Mandatory = $false)] + [switch]$IncludeNeverSignedIn, # Include users who have never signed in (default is to skip them), future use would allow this to be set in an alert configuration $TenantFilter ) @@ -45,10 +45,8 @@ function Get-CIPPAlertInactiveLicensedUsers { # Check if inactive $isInactive = (-not $lastSignIn) -or ([DateTime]$lastSignIn -le $Lookup) - - # Apply SkipNeverSignedIn filter #future use - if ($SkipNeverSignedIn -and -not $lastSignIn) { continue } - + # Skip users who have never signed in by default (unless IncludeNeverSignedIn is specified) + if (-not $IncludeNeverSignedIn -and -not $lastSignIn) { continue } # Only process inactive users if ($isInactive) { if (-not $lastSignIn) { From e8f5b495750f507aba5f34075496bb5007626a7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Thu, 5 Jun 2025 17:18:03 +0200 Subject: [PATCH 035/160] Refactor Invoke-EditRoomMailbox and Invoke-ListRooms scripts for consistency in parameter casing and logging. Updated property access from 'body' to 'Body' for uniformity. --- .../Resources/Invoke-EditRoomMailbox.ps1 | 10 ++- .../Resources/Invoke-ListRooms.ps1 | 78 +++++++++---------- 2 files changed, 45 insertions(+), 43 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-EditRoomMailbox.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-EditRoomMailbox.ps1 index 6e206791b411..2b3f42107b80 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-EditRoomMailbox.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-EditRoomMailbox.ps1 @@ -10,13 +10,15 @@ Function Invoke-EditRoomMailbox { [CmdletBinding()] param($Request, $TriggerMetadata) - $APIName = $TriggerMetadata.FunctionName - $Tenant = $Request.body.tenantid - Write-LogMessage -headers $Request.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' + + $Tenant = $Request.Body.tenantID $Results = [System.Collections.Generic.List[Object]]::new() - $MailboxObject = $Request.body + $MailboxObject = $Request.Body # First update the mailbox properties $UpdateMailboxParams = @{ diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-ListRooms.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-ListRooms.ps1 index ad7625bd61c6..fc58bfe47db8 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-ListRooms.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-ListRooms.ps1 @@ -46,57 +46,57 @@ Function Invoke-ListRooms { $GraphRequest = @( [PSCustomObject]@{ # Core Mailbox Properties - id = $RoomMailbox.ExternalDirectoryObjectId - displayName = $RoomMailbox.DisplayName - mail = $RoomMailbox.PrimarySmtpAddress - mailNickname = $RoomMailbox.Alias - accountDisabled = $RoomMailbox.AccountDisabled - hiddenFromAddressListsEnabled = $RoomMailbox.HiddenFromAddressListsEnabled - isDirSynced = $RoomMailbox.IsDirSynced + id = $RoomMailbox.ExternalDirectoryObjectId + displayName = $RoomMailbox.DisplayName + mail = $RoomMailbox.PrimarySmtpAddress + mailNickname = $RoomMailbox.Alias + accountDisabled = $RoomMailbox.AccountDisabled + hiddenFromAddressListsEnabled = $RoomMailbox.HiddenFromAddressListsEnabled + isDirSynced = $RoomMailbox.IsDirSynced # Room Booking Settings - bookingType = $PlaceDetails.BookingType - resourceDelegates = $PlaceDetails.ResourceDelegates - capacity = [int]($PlaceDetails.Capacity ?? $RoomMailbox.ResourceCapacity ?? 0) + bookingType = $PlaceDetails.BookingType + resourceDelegates = $PlaceDetails.ResourceDelegates + capacity = [int]($PlaceDetails.Capacity ?? $RoomMailbox.ResourceCapacity ?? 0) # Location Information - building = $PlaceDetails.Building - floor = $PlaceDetails.Floor - floorLabel = $PlaceDetails.FloorLabel - street = if ([string]::IsNullOrWhiteSpace($PlaceDetails.Street)) { $null } else { $PlaceDetails.Street } - city = if ([string]::IsNullOrWhiteSpace($PlaceDetails.City)) { $null } else { $PlaceDetails.City } - state = if ([string]::IsNullOrWhiteSpace($PlaceDetails.State)) { $null } else { $PlaceDetails.State } - postalCode = if ([string]::IsNullOrWhiteSpace($PlaceDetails.PostalCode)) { $null } else { $PlaceDetails.PostalCode } - countryOrRegion = if ([string]::IsNullOrWhiteSpace($PlaceDetails.CountryOrRegion)) { $null } else { $PlaceDetails.CountryOrRegion } + building = $PlaceDetails.Building + floor = $PlaceDetails.Floor + floorLabel = $PlaceDetails.FloorLabel + street = if ([string]::IsNullOrWhiteSpace($PlaceDetails.Street)) { $null } else { $PlaceDetails.Street } + city = if ([string]::IsNullOrWhiteSpace($PlaceDetails.City)) { $null } else { $PlaceDetails.City } + state = if ([string]::IsNullOrWhiteSpace($PlaceDetails.State)) { $null } else { $PlaceDetails.State } + postalCode = if ([string]::IsNullOrWhiteSpace($PlaceDetails.PostalCode)) { $null } else { $PlaceDetails.PostalCode } + countryOrRegion = if ([string]::IsNullOrWhiteSpace($PlaceDetails.CountryOrRegion)) { $null } else { $PlaceDetails.CountryOrRegion } # Room Equipment - audioDeviceName = $PlaceDetails.AudioDeviceName - videoDeviceName = $PlaceDetails.VideoDeviceName - displayDeviceName = $PlaceDetails.DisplayDeviceName - mtrEnabled = $PlaceDetails.MTREnabled + audioDeviceName = $PlaceDetails.AudioDeviceName + videoDeviceName = $PlaceDetails.VideoDeviceName + displayDeviceName = $PlaceDetails.DisplayDeviceName + mtrEnabled = $PlaceDetails.MTREnabled # Room Features - isWheelChairAccessible = $PlaceDetails.IsWheelChairAccessible - phone = if ([string]::IsNullOrWhiteSpace($PlaceDetails.Phone)) { $null } else { $PlaceDetails.Phone } - tags = $PlaceDetails.Tags - spaceType = $PlaceDetails.SpaceType + isWheelChairAccessible = $PlaceDetails.IsWheelChairAccessible + phone = if ([string]::IsNullOrWhiteSpace($PlaceDetails.Phone)) { $null } else { $PlaceDetails.Phone } + tags = $PlaceDetails.Tags + spaceType = $PlaceDetails.SpaceType # Calendar Properties - AllowConflicts = $CalendarProperties.AllowConflicts - AllowRecurringMeetings = $CalendarProperties.AllowRecurringMeetings - BookingWindowInDays = $CalendarProperties.BookingWindowInDays - MaximumDurationInMinutes = $CalendarProperties.MaximumDurationInMinutes - ProcessExternalMeetingMessages= $CalendarProperties.ProcessExternalMeetingMessages - EnforceCapacity = $CalendarProperties.EnforceCapacity - ForwardRequestsToDelegates = $CalendarProperties.ForwardRequestsToDelegates - ScheduleOnlyDuringWorkHours = $CalendarProperties.ScheduleOnlyDuringWorkHours - AutomateProcessing = $CalendarProperties.AutomateProcessing + AllowConflicts = $CalendarProperties.AllowConflicts + AllowRecurringMeetings = $CalendarProperties.AllowRecurringMeetings + BookingWindowInDays = $CalendarProperties.BookingWindowInDays + MaximumDurationInMinutes = $CalendarProperties.MaximumDurationInMinutes + ProcessExternalMeetingMessages = $CalendarProperties.ProcessExternalMeetingMessages + EnforceCapacity = $CalendarProperties.EnforceCapacity + ForwardRequestsToDelegates = $CalendarProperties.ForwardRequestsToDelegates + ScheduleOnlyDuringWorkHours = $CalendarProperties.ScheduleOnlyDuringWorkHours + AutomateProcessing = $CalendarProperties.AutomateProcessing # Calendar Configuration Properties - WorkDays = if ([string]::IsNullOrWhiteSpace($CalendarConfigurationProperties.WorkDays)) { $null } else { $CalendarConfigurationProperties.WorkDays } - WorkHoursStartTime = if ([string]::IsNullOrWhiteSpace($CalendarConfigurationProperties.WorkHoursStartTime)) { $null } else { $CalendarConfigurationProperties.WorkHoursStartTime } - WorkHoursEndTime = if ([string]::IsNullOrWhiteSpace($CalendarConfigurationProperties.WorkHoursEndTime)) { $null } else { $CalendarConfigurationProperties.WorkHoursEndTime } - WorkingHoursTimeZone = $CalendarConfigurationProperties.WorkingHoursTimeZone + WorkDays = if ([string]::IsNullOrWhiteSpace($CalendarConfigurationProperties.WorkDays)) { $null } else { $CalendarConfigurationProperties.WorkDays } + WorkHoursStartTime = if ([string]::IsNullOrWhiteSpace($CalendarConfigurationProperties.WorkHoursStartTime)) { $null } else { $CalendarConfigurationProperties.WorkHoursStartTime } + WorkHoursEndTime = if ([string]::IsNullOrWhiteSpace($CalendarConfigurationProperties.WorkHoursEndTime)) { $null } else { $CalendarConfigurationProperties.WorkHoursEndTime } + WorkingHoursTimeZone = $CalendarConfigurationProperties.WorkingHoursTimeZone } ) } From b13f6d3ac7b82f6402a19a54fe3818e15ab3c308 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Thu, 5 Jun 2025 17:18:41 +0200 Subject: [PATCH 036/160] Add equipment backend functions Fix logging header references in Invoke-AddEquipmentMailbox function for consistency parameter renaming Remove unused properties from equipment details in Invoke-ListEquipment function Refactor Invoke-AddEquipmentMailbox and Invoke-AddRoomMailbox functions for improved error handling and logging consistency. Updated mailbox parameter names for clarity. --- .../Resources/Invoke-AddEquipmentMailbox.ps1 | 62 ++++++++++ .../Resources/Invoke-AddRoomMailbox.ps1 | 8 +- .../Resources/Invoke-EditEquipmentMailbox.ps1 | 115 ++++++++++++++++++ .../Resources/Invoke-ListEquipment.ps1 | 99 +++++++++++++++ 4 files changed, 281 insertions(+), 3 deletions(-) create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-AddEquipmentMailbox.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-EditEquipmentMailbox.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-ListEquipment.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-AddEquipmentMailbox.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-AddEquipmentMailbox.ps1 new file mode 100644 index 000000000000..93497681a93a --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-AddEquipmentMailbox.ps1 @@ -0,0 +1,62 @@ +using namespace System.Net + +Function Invoke-AddEquipmentMailbox { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Exchange.Equipment.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + + $Tenant = $Request.Body.tenantID + + $Results = [System.Collections.Generic.List[Object]]::new() + $MailboxObject = $Request.Body + + # Create the equipment mailbox + $NewMailboxParams = @{ + Name = $MailboxObject.username + DisplayName = $MailboxObject.displayName + Equipment = $true + PrimarySmtpAddress = $MailboxObject.userPrincipalName + } + + try { + # Create the equipment mailbox + $AddEquipmentRequest = New-ExoRequest -tenantid $Tenant -cmdlet 'New-Mailbox' -cmdParams $NewMailboxParams + $Results.Add("Successfully created equipment mailbox: $($MailboxObject.displayName)") + + # Block sign-in for the mailbox + try { + $BlockSignInRequest = Set-CIPPSignInState -userid $AddEquipmentRequest.ExternalDirectoryObjectId -TenantFilter $Tenant -APIName $APINAME -Headers $Headers -AccountEnabled $false + if ($BlockSignInRequest -like 'Could not disable*') { throw $BlockSignInRequest } + $Results.Add("Blocked sign-in for Equipment mailbox; $($MailboxObject.userPrincipalName)") + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Results.Add("Failed to block sign-in for Equipment mailbox: $($MailboxObject.userPrincipalName). Error: $($ErrorMessage.NormalizedError)") + } + Write-LogMessage -headers $Headers -API $APIName -tenant $Tenant -message "Created equipment mailbox $($MailboxObject.displayName)" -Sev 'Info' + $StatusCode = [HttpStatusCode]::OK + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Message = "Failed to create equipment mailbox: $($MailboxObject.displayName). Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -tenant $Tenant -message $Message -Sev 'Error' -LogData $ErrorMessage + $Results.Add($Message) + $StatusCode = [HttpStatusCode]::Forbidden + } + + $Body = [pscustomobject]@{ 'Results' = @($Results) } + + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = $Body + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-AddRoomMailbox.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-AddRoomMailbox.ps1 index 0b7f139a7832..d174b644ab39 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-AddRoomMailbox.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-AddRoomMailbox.ps1 @@ -32,7 +32,8 @@ Function Invoke-AddRoomMailbox { # Block sign-in for the mailbox try { - $Request = Set-CIPPSignInState -userid $AddRoomRequest.ExternalDirectoryObjectId -TenantFilter $Tenant -APIName $APINAME -Headers $Headers -AccountEnabled $false + $BlockSignInRequest = Set-CIPPSignInState -userid $AddRoomRequest.ExternalDirectoryObjectId -TenantFilter $Tenant -APIName $APINAME -Headers $Headers -AccountEnabled $false + if ($BlockSignInRequest -like 'Could not disable*') { throw $BlockSignInRequest } $Results.Add("Blocked sign-in for Room mailbox; $($MailboxObject.userPrincipalName)") } catch { $ErrorMessage = Get-CippException -Exception $_ @@ -41,8 +42,9 @@ Function Invoke-AddRoomMailbox { $StatusCode = [HttpStatusCode]::OK } catch { $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -Headers $Headers -API $APIName -tenant $Tenant -message "Failed to create room: $($MailboxObject.DisplayName). Error: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage - $Results.Add("Failed to create Room mailbox $($MailboxObject.userPrincipalName). $($ErrorMessage.NormalizedError)") + $Message = "Failed to create room mailbox: $($MailboxObject.DisplayName). Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -Headers $Headers -API $APIName -tenant $Tenant -message $Message -Sev 'Error' -LogData $ErrorMessage + $Results.Add($Message) $StatusCode = [HttpStatusCode]::InternalServerError } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-EditEquipmentMailbox.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-EditEquipmentMailbox.ps1 new file mode 100644 index 000000000000..ec80257aa684 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-EditEquipmentMailbox.ps1 @@ -0,0 +1,115 @@ +using namespace System.Net + +Function Invoke-EditEquipmentMailbox { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Exchange.Equipment.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + $Tenant = $Request.Body.tenantID + + $Results = [System.Collections.Generic.List[Object]]::new() + $MailboxObject = $Request.Body + + # First update the mailbox properties + $UpdateMailboxParams = @{ + Identity = $MailboxObject.equipmentId + DisplayName = $MailboxObject.displayName + } + + if (![string]::IsNullOrWhiteSpace($MailboxObject.hiddenFromAddressListsEnabled)) { + $UpdateMailboxParams.Add('HiddenFromAddressListsEnabled', $MailboxObject.hiddenFromAddressListsEnabled) + } + + # Then update the user properties + $UpdateUserParams = @{ + Identity = $MailboxObject.equipmentId + } + + # Add optional parameters if they exist + $UserProperties = @( + 'Location', 'Department', 'Company', + 'Phone', 'Tags', + 'StreetAddress', 'City', 'StateOrProvince', 'CountryOrRegion', + 'PostalCode' + ) + + foreach ($prop in $UserProperties) { + if (![string]::IsNullOrWhiteSpace($MailboxObject.$prop)) { + $UpdateUserParams[$prop] = $MailboxObject.$prop + } + } + + # Then update the calendar properties + $UpdateCalendarParams = @{ + Identity = $MailboxObject.equipmentId + } + + $CalendarProperties = @( + 'AllowConflicts', 'AllowRecurringMeetings', 'BookingWindowInDays', + 'MaximumDurationInMinutes', 'ProcessExternalMeetingMessages', + 'ForwardRequestsToDelegates', 'ScheduleOnlyDuringWorkHours', 'AutomateProcessing' + ) + + foreach ($prop in $CalendarProperties) { + if (![string]::IsNullOrWhiteSpace($MailboxObject.$prop)) { + $UpdateCalendarParams[$prop] = $MailboxObject.$prop + } + } + + # Then update the calendar configuration + $UpdateCalendarConfigParams = @{ + Identity = $MailboxObject.equipmentId + } + + $CalendarConfiguration = @( + 'WorkDays', 'WorkHoursStartTime', 'WorkHoursEndTime', 'WorkingHoursTimeZone' + ) + + foreach ($prop in $CalendarConfiguration) { + if (![string]::IsNullOrWhiteSpace($MailboxObject.$prop)) { + $UpdateCalendarConfigParams[$prop] = $MailboxObject.$prop + } + } + + try { + # Update mailbox properties + $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-Mailbox' -cmdParams $UpdateMailboxParams + + # Update user properties + $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-User' -cmdParams $UpdateUserParams + $Results.Add("Successfully updated equipment: $($MailboxObject.DisplayName) (User Properties)") + + # Update calendar properties + $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-CalendarProcessing' -cmdParams $UpdateCalendarParams + $Results.Add("Successfully updated equipment: $($MailboxObject.DisplayName) (Calendar Properties)") + + # Update calendar configuration properties + $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-MailboxCalendarConfiguration' -cmdParams $UpdateCalendarConfigParams + $Results.Add("Successfully updated equipment: $($MailboxObject.DisplayName) (Calendar Configuration)") + + Write-LogMessage -headers $Headers -API $APIName -tenant $Tenant -message "Updated equipment $($MailboxObject.DisplayName)" -Sev 'Info' + $StatusCode = [HttpStatusCode]::OK + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -headers $Headers -API $APIName -tenant $Tenant -message "Failed to update equipment: $($MailboxObject.DisplayName). Error: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage + $Results.Add("Failed to update Equipment mailbox $($MailboxObject.userPrincipalName). $($ErrorMessage.NormalizedError)") + $StatusCode = [HttpStatusCode]::Forbidden + } + + $Body = [pscustomobject]@{ 'Results' = @($Results) } + + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = $Body + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-ListEquipment.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-ListEquipment.ps1 new file mode 100644 index 000000000000..fe890424f3ed --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-ListEquipment.ps1 @@ -0,0 +1,99 @@ +using namespace System.Net + +Function Invoke-ListEquipment { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Exchange.Equipment.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + + $EquipmentId = $Request.Query.EquipmentId + $Tenant = $Request.Query.TenantFilter + + try { + if ($EquipmentId) { + # Get specific equipment details + $Equipment = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-Mailbox' -cmdParams @{ + Identity = $EquipmentId + RecipientTypeDetails = 'EquipmentMailbox' + } + + $UserDetails = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-User' -cmdParams @{ + Identity = $EquipmentId + } + + $CalendarProcessing = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-CalendarProcessing' -cmdParams @{ + Identity = $EquipmentId + } + + $CalendarConfig = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-MailboxCalendarConfiguration' -cmdParams @{ + Identity = $EquipmentId + } + + $Results = [PSCustomObject]@{ + # Core mailbox properties + displayName = $Equipment.DisplayName + hiddenFromAddressListsEnabled = $Equipment.HiddenFromAddressListsEnabled + userPrincipalName = $Equipment.UserPrincipalName + primarySmtpAddress = $Equipment.PrimarySmtpAddress + + # Equipment details from Get-User + department = $UserDetails.Department + company = $UserDetails.Company + + # Location information from Get-User + street = $UserDetails.Street + city = $UserDetails.City + state = $UserDetails.State + postalCode = $UserDetails.PostalCode + countryOrRegion = $UserDetails.CountryOrRegion + + # Equipment features + phone = $UserDetails.Phone + tags = $UserDetails.Tags + + # Calendar properties from Get-CalendarProcessing + allowConflicts = $CalendarProcessing.AllowConflicts + allowRecurringMeetings = $CalendarProcessing.AllowRecurringMeetings + bookingWindowInDays = $CalendarProcessing.BookingWindowInDays + maximumDurationInMinutes = $CalendarProcessing.MaximumDurationInMinutes + processExternalMeetingMessages = $CalendarProcessing.ProcessExternalMeetingMessages + forwardRequestsToDelegates = $CalendarProcessing.ForwardRequestsToDelegates + scheduleOnlyDuringWorkHours = $CalendarProcessing.ScheduleOnlyDuringWorkHours + automateProcessing = $CalendarProcessing.AutomateProcessing + + # Calendar configuration from Get-MailboxCalendarConfiguration + workDays = $CalendarConfig.WorkDays + workHoursStartTime = $CalendarConfig.WorkHoursStartTime + workHoursEndTime = $CalendarConfig.WorkHoursEndTime + workingHoursTimeZone = $CalendarConfig.WorkingHoursTimeZone + } + } else { + # List all equipment mailboxes + $Results = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-Mailbox' -cmdParams @{ + RecipientTypeDetails = 'EquipmentMailbox' + ResultSize = 'Unlimited' + } | Select-Object -ExcludeProperty *data.type* + } + $StatusCode = [HttpStatusCode]::OK + + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + $StatusCode = [HttpStatusCode]::Forbidden + $Results = $ErrorMessage + } + + + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @($Results | Sort-Object displayName) + }) +} From e171f971fa04ffbfa58009f963bd094c7ffaabf6 Mon Sep 17 00:00:00 2001 From: Jr7468 Date: Fri, 6 Jun 2025 00:29:04 +0100 Subject: [PATCH 037/160] Added Calendar Processing --- .../Invoke-ExecSetCalendarProcessing.ps1 | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecSetCalendarProcessing.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecSetCalendarProcessing.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecSetCalendarProcessing.ps1 new file mode 100644 index 000000000000..5853374618ac --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecSetCalendarProcessing.ps1 @@ -0,0 +1,66 @@ +using namespace System.Net + +function Invoke-ExecSetCalendarProcessing { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Exchange.Mailbox.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = 'ExecSetCalendarProcessing' + Write-LogMessage -Headers $Request.Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + + try { + $cmdParams = @{ + Identity = $Request.Body.UPN + AutomateProcessing = if ($Request.Body.automaticallyAccept -as [bool]) { 'AutoAccept' } elseif ($Request.Body.automaticallyProcess -as [bool]) { 'AutoUpdate' } else { 'None' } + AllowConflicts = $Request.Body.allowConflicts -as [bool] + AllowRecurringMeetings = $Request.Body.allowRecurringMeetings -as [bool] + ScheduleOnlyDuringWorkHours = $Request.Body.scheduleOnlyDuringWorkHours -as [bool] + AddOrganizerToSubject = $Request.Body.addOrganizerToSubject -as [bool] + DeleteComments = $Request.Body.deleteComments -as [bool] + DeleteSubject = $Request.Body.deleteSubject -as [bool] + RemovePrivateProperty = $Request.Body.removePrivateProperty -as [bool] + RemoveCanceledMeetings = $Request.Body.removeCanceledMeetings -as [bool] + RemoveOldMeetingMessages = $Request.Body.removeOldMeetingMessages -as [bool] + ProcessExternalMeetingMessages = $Request.Body.processExternalMeetingMessages -as [bool] + } + + # Add optional numeric parameters only if they have values + if ($Request.Body.maxConflicts) { + $cmdParams['MaximumConflictInstances'] = $Request.Body.maxConflicts -as [int] + } + if ($Request.Body.maximumDurationInMinutes) { + $cmdParams['MaximumDurationInMinutes'] = $Request.Body.maximumDurationInMinutes -as [int] + } + if ($Request.Body.minimumDurationInMinutes) { + $cmdParams['MinimumDurationInMinutes'] = $Request.Body.minimumDurationInMinutes -as [int] + } + if ($Request.Body.bookingWindowInDays) { + $cmdParams['BookingWindowInDays'] = $Request.Body.bookingWindowInDays -as [int] + } + if ($Request.Body.additionalResponse) { + $cmdParams['AdditionalResponse'] = $Request.Body.additionalResponse + } + + $null = New-ExoRequest -tenantid $Request.Body.tenantFilter -cmdlet 'Set-CalendarProcessing' -cmdParams $cmdParams + + $Results = "Calendar processing settings for $($Request.Body.UPN) have been updated successfully" + Write-LogMessage -API $APIName -tenant $Request.Body.tenantFilter -message $Results -sev Info + $StatusCode = [HttpStatusCode]::OK + } + catch { + $ErrorMessage = Get-CippException -Exception $_ + $Results = "Could not update calendar processing settings for $($Request.Body.UPN). Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -API $APIName -tenant $Request.Body.tenantFilter -message $Results -sev Error -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::InternalServerError + } + + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{ Results = $Results } + }) +} \ No newline at end of file From 5285821b092386988f23392e86a3eb25d712b721 Mon Sep 17 00:00:00 2001 From: Jack Date: Fri, 6 Jun 2025 10:25:46 +0100 Subject: [PATCH 038/160] Created a Standard to enable Name Pronounciation --- ...e-CIPPStandardEnableNamePronounciation.ps1 | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableNamePronounciation.ps1 diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableNamePronounciation.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableNamePronounciation.ps1 new file mode 100644 index 000000000000..3a10fd7ba2ca --- /dev/null +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableNamePronounciation.ps1 @@ -0,0 +1,73 @@ +function Invoke-CIPPStandardEnableNamePronounciation { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) EnableNamePronounciation + .SYNOPSIS + (Label) Enable Name Pronounciation + .DESCRIPTION + (Helptext) Enables the Name Pronounciation feature for the tenant. This allows users to set their name pronounciation in their profile. + (DocsDescription) Enables the Name Pronounciation feature for the tenant. This allows users to set their name pronounciation in their profile. + .NOTES + CAT + Global Standards + TAG + ADDEDCOMPONENT + IMPACT + Low Impact + ADDEDDATE + 2025-06-06 + RECOMMENDEDBY + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + .LINK + https://docs.cipp.app/user-documentation/tenant/standards/list-standards + #> + + param ($Tenant, $Settings) + ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'EnablePronouns' + + $Uri = 'https://graph.microsoft.com/v1.0/admin/people/namePronunciation' + try { + $CurrentState = New-GraphGetRequest -Uri $Uri -tenantid $Tenant + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Could not get CurrentState for Name Pronounciation. Error: $($ErrorMessage.NormalizedError)" -sev Error + Return + } + Write-Host $CurrentState + + if ($Settings.remediate -eq $true) { + Write-Host 'Time to remediate' + + if ($CurrentState.isEnabledInOrganization -eq $true) { + Write-LogMessage -API 'Standards' -tenant $tenant -message 'Name Pronounciation is already enabled.' -sev Info + } else { + $CurrentState.isEnabledInOrganization = $true + try { + $Body = ConvertTo-Json -InputObject $CurrentState -Depth 10 -Compress + New-GraphPostRequest -Uri $Uri -tenantid $Tenant -Body $Body -type PATCH + Write-LogMessage -API 'Standards' -tenant $tenant -message 'Enabled name pronounciation.' -sev Info + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to enable name pronounciation. Error: $ErrorMessage" -sev Error + } + } + } + + if ($Settings.alert -eq $true) { + + if ($CurrentState.isEnabledInOrganization -eq $true) { + Write-LogMessage -API 'Standards' -tenant $tenant -message 'Name Pronounciation is enabled.' -sev Info + } else { + Write-StandardsAlert -message 'Name Pronounciation is not enabled' -object $CurrentState -tenant $tenant -standardName 'EnableNamePronounciation' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $tenant -message 'Name Pronounciation is not enabled.' -sev Info + } + } + + if ($Settings.report -eq $true) { + Set-CIPPStandardsCompareField -FieldName 'standards.EnableNamePronounciation' -FieldValue $CurrentState.isEnabledInOrganization -Tenant $tenant + Add-CIPPBPAField -FieldName 'NamePronounciationEnabled' -FieldValue $CurrentState.isEnabledInOrganization -StoreAs bool -Tenant $tenant + } +} From 1ee029983dfe1ad012e3bdbe8f0c79c200dead45 Mon Sep 17 00:00:00 2001 From: Jack Date: Fri, 6 Jun 2025 10:28:50 +0100 Subject: [PATCH 039/160] Spelling --- ...e-CIPPStandardEnableNamePronunciation.ps1} | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) rename Modules/CIPPCore/Public/Standards/{Invoke-CIPPStandardEnableNamePronounciation.ps1 => Invoke-CIPPStandardEnableNamePronunciation.ps1} (59%) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableNamePronounciation.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableNamePronunciation.ps1 similarity index 59% rename from Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableNamePronounciation.ps1 rename to Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableNamePronunciation.ps1 index 3a10fd7ba2ca..1cb711683b19 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableNamePronounciation.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableNamePronunciation.ps1 @@ -1,14 +1,14 @@ -function Invoke-CIPPStandardEnableNamePronounciation { +function Invoke-CIPPStandardEnableNamePronunciation { <# .FUNCTIONALITY Internal .COMPONENT - (APIName) EnableNamePronounciation + (APIName) EnableNamePronunciation .SYNOPSIS - (Label) Enable Name Pronounciation + (Label) Enable Name Pronunciation .DESCRIPTION - (Helptext) Enables the Name Pronounciation feature for the tenant. This allows users to set their name pronounciation in their profile. - (DocsDescription) Enables the Name Pronounciation feature for the tenant. This allows users to set their name pronounciation in their profile. + (Helptext) Enables the Name Pronunciation feature for the tenant. This allows users to set their name pronunciation in their profile. + (DocsDescription) Enables the Name Pronunciation feature for the tenant. This allows users to set their name pronunciation in their profile. .NOTES CAT Global Standards @@ -26,14 +26,13 @@ function Invoke-CIPPStandardEnableNamePronounciation { #> param ($Tenant, $Settings) - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'EnablePronouns' $Uri = 'https://graph.microsoft.com/v1.0/admin/people/namePronunciation' try { $CurrentState = New-GraphGetRequest -Uri $Uri -tenantid $Tenant } catch { $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Could not get CurrentState for Name Pronounciation. Error: $($ErrorMessage.NormalizedError)" -sev Error + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Could not get CurrentState for Name Pronunciation. Error: $($ErrorMessage.NormalizedError)" -sev Error Return } Write-Host $CurrentState @@ -42,16 +41,16 @@ function Invoke-CIPPStandardEnableNamePronounciation { Write-Host 'Time to remediate' if ($CurrentState.isEnabledInOrganization -eq $true) { - Write-LogMessage -API 'Standards' -tenant $tenant -message 'Name Pronounciation is already enabled.' -sev Info + Write-LogMessage -API 'Standards' -tenant $tenant -message 'Name Pronunciation is already enabled.' -sev Info } else { $CurrentState.isEnabledInOrganization = $true try { $Body = ConvertTo-Json -InputObject $CurrentState -Depth 10 -Compress New-GraphPostRequest -Uri $Uri -tenantid $Tenant -Body $Body -type PATCH - Write-LogMessage -API 'Standards' -tenant $tenant -message 'Enabled name pronounciation.' -sev Info + Write-LogMessage -API 'Standards' -tenant $tenant -message 'Enabled name pronunciation.' -sev Info } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to enable name pronounciation. Error: $ErrorMessage" -sev Error + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to enable name pronunciation. Error: $ErrorMessage" -sev Error } } } @@ -59,15 +58,15 @@ function Invoke-CIPPStandardEnableNamePronounciation { if ($Settings.alert -eq $true) { if ($CurrentState.isEnabledInOrganization -eq $true) { - Write-LogMessage -API 'Standards' -tenant $tenant -message 'Name Pronounciation is enabled.' -sev Info + Write-LogMessage -API 'Standards' -tenant $tenant -message 'Name Pronunciation is enabled.' -sev Info } else { - Write-StandardsAlert -message 'Name Pronounciation is not enabled' -object $CurrentState -tenant $tenant -standardName 'EnableNamePronounciation' -standardId $Settings.standardId - Write-LogMessage -API 'Standards' -tenant $tenant -message 'Name Pronounciation is not enabled.' -sev Info + Write-StandardsAlert -message 'Name Pronunciation is not enabled' -object $CurrentState -tenant $tenant -standardName 'EnableNamePronunciation' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $tenant -message 'Name Pronunciation is not enabled.' -sev Info } } if ($Settings.report -eq $true) { - Set-CIPPStandardsCompareField -FieldName 'standards.EnableNamePronounciation' -FieldValue $CurrentState.isEnabledInOrganization -Tenant $tenant - Add-CIPPBPAField -FieldName 'NamePronounciationEnabled' -FieldValue $CurrentState.isEnabledInOrganization -StoreAs bool -Tenant $tenant + Set-CIPPStandardsCompareField -FieldName 'standards.EnableNamePronunciation' -FieldValue $CurrentState.isEnabledInOrganization -Tenant $tenant + Add-CIPPBPAField -FieldName 'NamePronunciationEnabled' -FieldValue $CurrentState.isEnabledInOrganization -StoreAs bool -Tenant $tenant } } From 2913ea2a581cdd959b6fa1a75f13a080a882806f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Fri, 6 Jun 2025 11:30:11 +0200 Subject: [PATCH 040/160] fix: Authorization failed because of missing requirement(s). error and better logging --- .../Standards/Invoke-CIPPStandardEnablePronouns.ps1 | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnablePronouns.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnablePronouns.ps1 index e3c27cd633ad..3e3a23a6ee9a 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnablePronouns.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnablePronouns.ps1 @@ -35,10 +35,9 @@ function Invoke-CIPPStandardEnablePronouns { $CurrentState = New-GraphGetRequest -Uri $Uri -tenantid $Tenant } catch { $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Could not get CurrentState for Pronouns. Error: $($ErrorMessage.NormalizedError)" -sev Error + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Could not get CurrentState for Pronouns. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage Return } - Write-Host $CurrentState if ($Settings.remediate -eq $true) { Write-Host 'Time to remediate' @@ -49,11 +48,11 @@ function Invoke-CIPPStandardEnablePronouns { $CurrentState.isEnabledInOrganization = $true try { $Body = ConvertTo-Json -InputObject $CurrentState -Depth 10 -Compress - New-GraphPostRequest -Uri $Uri -tenantid $Tenant -Body $Body -type PATCH + $null = New-GraphPostRequest -Uri $Uri -tenantid $Tenant -Body $Body -type PATCH -AsApp $true Write-LogMessage -API 'Standards' -tenant $tenant -message 'Enabled pronouns.' -sev Info } catch { - $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to enable pronouns. Error: $ErrorMessage" -sev Error + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to enable pronouns. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage } } } From b73689afeb1d4395021339259770b7f17a4ed5c9 Mon Sep 17 00:00:00 2001 From: Jack Date: Fri, 6 Jun 2025 10:25:46 +0100 Subject: [PATCH 041/160] Created a Standard to enable Name Pronounciation --- ...ke-CIPPStandardEnableNamePronunciation.ps1 | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableNamePronunciation.ps1 diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableNamePronunciation.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableNamePronunciation.ps1 new file mode 100644 index 000000000000..1cb711683b19 --- /dev/null +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableNamePronunciation.ps1 @@ -0,0 +1,72 @@ +function Invoke-CIPPStandardEnableNamePronunciation { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) EnableNamePronunciation + .SYNOPSIS + (Label) Enable Name Pronunciation + .DESCRIPTION + (Helptext) Enables the Name Pronunciation feature for the tenant. This allows users to set their name pronunciation in their profile. + (DocsDescription) Enables the Name Pronunciation feature for the tenant. This allows users to set their name pronunciation in their profile. + .NOTES + CAT + Global Standards + TAG + ADDEDCOMPONENT + IMPACT + Low Impact + ADDEDDATE + 2025-06-06 + RECOMMENDEDBY + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + .LINK + https://docs.cipp.app/user-documentation/tenant/standards/list-standards + #> + + param ($Tenant, $Settings) + + $Uri = 'https://graph.microsoft.com/v1.0/admin/people/namePronunciation' + try { + $CurrentState = New-GraphGetRequest -Uri $Uri -tenantid $Tenant + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Could not get CurrentState for Name Pronunciation. Error: $($ErrorMessage.NormalizedError)" -sev Error + Return + } + Write-Host $CurrentState + + if ($Settings.remediate -eq $true) { + Write-Host 'Time to remediate' + + if ($CurrentState.isEnabledInOrganization -eq $true) { + Write-LogMessage -API 'Standards' -tenant $tenant -message 'Name Pronunciation is already enabled.' -sev Info + } else { + $CurrentState.isEnabledInOrganization = $true + try { + $Body = ConvertTo-Json -InputObject $CurrentState -Depth 10 -Compress + New-GraphPostRequest -Uri $Uri -tenantid $Tenant -Body $Body -type PATCH + Write-LogMessage -API 'Standards' -tenant $tenant -message 'Enabled name pronunciation.' -sev Info + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to enable name pronunciation. Error: $ErrorMessage" -sev Error + } + } + } + + if ($Settings.alert -eq $true) { + + if ($CurrentState.isEnabledInOrganization -eq $true) { + Write-LogMessage -API 'Standards' -tenant $tenant -message 'Name Pronunciation is enabled.' -sev Info + } else { + Write-StandardsAlert -message 'Name Pronunciation is not enabled' -object $CurrentState -tenant $tenant -standardName 'EnableNamePronunciation' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $tenant -message 'Name Pronunciation is not enabled.' -sev Info + } + } + + if ($Settings.report -eq $true) { + Set-CIPPStandardsCompareField -FieldName 'standards.EnableNamePronunciation' -FieldValue $CurrentState.isEnabledInOrganization -Tenant $tenant + Add-CIPPBPAField -FieldName 'NamePronunciationEnabled' -FieldValue $CurrentState.isEnabledInOrganization -StoreAs bool -Tenant $tenant + } +} From 902df69fb668361ee43b460f4e54398f22a12d33 Mon Sep 17 00:00:00 2001 From: Jack Date: Fri, 6 Jun 2025 10:39:10 +0100 Subject: [PATCH 042/160] Fixed -AsApp $true --- .../Standards/Invoke-CIPPStandardEnableNamePronunciation.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableNamePronunciation.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableNamePronunciation.ps1 index 1cb711683b19..6bf485ebb722 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableNamePronunciation.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableNamePronunciation.ps1 @@ -27,7 +27,7 @@ function Invoke-CIPPStandardEnableNamePronunciation { param ($Tenant, $Settings) - $Uri = 'https://graph.microsoft.com/v1.0/admin/people/namePronunciation' + $Uri = 'https://graph.microsoft.com/beta/admin/people/namePronunciation' try { $CurrentState = New-GraphGetRequest -Uri $Uri -tenantid $Tenant } catch { @@ -46,7 +46,7 @@ function Invoke-CIPPStandardEnableNamePronunciation { $CurrentState.isEnabledInOrganization = $true try { $Body = ConvertTo-Json -InputObject $CurrentState -Depth 10 -Compress - New-GraphPostRequest -Uri $Uri -tenantid $Tenant -Body $Body -type PATCH + $null = New-GraphPostRequest -Uri $Uri -tenantid $Tenant -Body $Body -type PATCH -AsApp $true Write-LogMessage -API 'Standards' -tenant $tenant -message 'Enabled name pronunciation.' -sev Info } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message From ae3371bc35041ba80df42f6172a6a47eb59a8e62 Mon Sep 17 00:00:00 2001 From: Jack Date: Fri, 6 Jun 2025 10:25:46 +0100 Subject: [PATCH 043/160] Created a Standard to enable Name Pronounciation --- ...ke-CIPPStandardEnableNamePronunciation.ps1 | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableNamePronunciation.ps1 diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableNamePronunciation.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableNamePronunciation.ps1 new file mode 100644 index 000000000000..6bf485ebb722 --- /dev/null +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableNamePronunciation.ps1 @@ -0,0 +1,72 @@ +function Invoke-CIPPStandardEnableNamePronunciation { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) EnableNamePronunciation + .SYNOPSIS + (Label) Enable Name Pronunciation + .DESCRIPTION + (Helptext) Enables the Name Pronunciation feature for the tenant. This allows users to set their name pronunciation in their profile. + (DocsDescription) Enables the Name Pronunciation feature for the tenant. This allows users to set their name pronunciation in their profile. + .NOTES + CAT + Global Standards + TAG + ADDEDCOMPONENT + IMPACT + Low Impact + ADDEDDATE + 2025-06-06 + RECOMMENDEDBY + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + .LINK + https://docs.cipp.app/user-documentation/tenant/standards/list-standards + #> + + param ($Tenant, $Settings) + + $Uri = 'https://graph.microsoft.com/beta/admin/people/namePronunciation' + try { + $CurrentState = New-GraphGetRequest -Uri $Uri -tenantid $Tenant + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Could not get CurrentState for Name Pronunciation. Error: $($ErrorMessage.NormalizedError)" -sev Error + Return + } + Write-Host $CurrentState + + if ($Settings.remediate -eq $true) { + Write-Host 'Time to remediate' + + if ($CurrentState.isEnabledInOrganization -eq $true) { + Write-LogMessage -API 'Standards' -tenant $tenant -message 'Name Pronunciation is already enabled.' -sev Info + } else { + $CurrentState.isEnabledInOrganization = $true + try { + $Body = ConvertTo-Json -InputObject $CurrentState -Depth 10 -Compress + $null = New-GraphPostRequest -Uri $Uri -tenantid $Tenant -Body $Body -type PATCH -AsApp $true + Write-LogMessage -API 'Standards' -tenant $tenant -message 'Enabled name pronunciation.' -sev Info + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to enable name pronunciation. Error: $ErrorMessage" -sev Error + } + } + } + + if ($Settings.alert -eq $true) { + + if ($CurrentState.isEnabledInOrganization -eq $true) { + Write-LogMessage -API 'Standards' -tenant $tenant -message 'Name Pronunciation is enabled.' -sev Info + } else { + Write-StandardsAlert -message 'Name Pronunciation is not enabled' -object $CurrentState -tenant $tenant -standardName 'EnableNamePronunciation' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $tenant -message 'Name Pronunciation is not enabled.' -sev Info + } + } + + if ($Settings.report -eq $true) { + Set-CIPPStandardsCompareField -FieldName 'standards.EnableNamePronunciation' -FieldValue $CurrentState.isEnabledInOrganization -Tenant $tenant + Add-CIPPBPAField -FieldName 'NamePronunciationEnabled' -FieldValue $CurrentState.isEnabledInOrganization -StoreAs bool -Tenant $tenant + } +} From 033010c1038bd00dcb3176da4a1950e9c4878234 Mon Sep 17 00:00:00 2001 From: Jack Date: Fri, 6 Jun 2025 10:25:46 +0100 Subject: [PATCH 044/160] Created a Standard to enable Name Pronounciation --- ...ke-CIPPStandardEnableNamePronunciation.ps1 | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableNamePronunciation.ps1 diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableNamePronunciation.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableNamePronunciation.ps1 new file mode 100644 index 000000000000..6bf485ebb722 --- /dev/null +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableNamePronunciation.ps1 @@ -0,0 +1,72 @@ +function Invoke-CIPPStandardEnableNamePronunciation { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) EnableNamePronunciation + .SYNOPSIS + (Label) Enable Name Pronunciation + .DESCRIPTION + (Helptext) Enables the Name Pronunciation feature for the tenant. This allows users to set their name pronunciation in their profile. + (DocsDescription) Enables the Name Pronunciation feature for the tenant. This allows users to set their name pronunciation in their profile. + .NOTES + CAT + Global Standards + TAG + ADDEDCOMPONENT + IMPACT + Low Impact + ADDEDDATE + 2025-06-06 + RECOMMENDEDBY + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + .LINK + https://docs.cipp.app/user-documentation/tenant/standards/list-standards + #> + + param ($Tenant, $Settings) + + $Uri = 'https://graph.microsoft.com/beta/admin/people/namePronunciation' + try { + $CurrentState = New-GraphGetRequest -Uri $Uri -tenantid $Tenant + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Could not get CurrentState for Name Pronunciation. Error: $($ErrorMessage.NormalizedError)" -sev Error + Return + } + Write-Host $CurrentState + + if ($Settings.remediate -eq $true) { + Write-Host 'Time to remediate' + + if ($CurrentState.isEnabledInOrganization -eq $true) { + Write-LogMessage -API 'Standards' -tenant $tenant -message 'Name Pronunciation is already enabled.' -sev Info + } else { + $CurrentState.isEnabledInOrganization = $true + try { + $Body = ConvertTo-Json -InputObject $CurrentState -Depth 10 -Compress + $null = New-GraphPostRequest -Uri $Uri -tenantid $Tenant -Body $Body -type PATCH -AsApp $true + Write-LogMessage -API 'Standards' -tenant $tenant -message 'Enabled name pronunciation.' -sev Info + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to enable name pronunciation. Error: $ErrorMessage" -sev Error + } + } + } + + if ($Settings.alert -eq $true) { + + if ($CurrentState.isEnabledInOrganization -eq $true) { + Write-LogMessage -API 'Standards' -tenant $tenant -message 'Name Pronunciation is enabled.' -sev Info + } else { + Write-StandardsAlert -message 'Name Pronunciation is not enabled' -object $CurrentState -tenant $tenant -standardName 'EnableNamePronunciation' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $tenant -message 'Name Pronunciation is not enabled.' -sev Info + } + } + + if ($Settings.report -eq $true) { + Set-CIPPStandardsCompareField -FieldName 'standards.EnableNamePronunciation' -FieldValue $CurrentState.isEnabledInOrganization -Tenant $tenant + Add-CIPPBPAField -FieldName 'NamePronunciationEnabled' -FieldValue $CurrentState.isEnabledInOrganization -StoreAs bool -Tenant $tenant + } +} From e05481116eec22b75400ca6fc1a03edeedd3045c Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Fri, 6 Jun 2025 19:40:24 +0800 Subject: [PATCH 045/160] Account for all AccessRight permissions when removing permissions, also account for multiple permissions in 1 request --- .../Invoke-ExecModifyMBPerms.ps1 | 187 ++++++++++++------ 1 file changed, 126 insertions(+), 61 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecModifyMBPerms.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecModifyMBPerms.ps1 index 222c3561e579..cddc705a3556 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecModifyMBPerms.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecModifyMBPerms.ps1 @@ -12,13 +12,13 @@ Function Invoke-ExecModifyMBPerms { $APIName = $Request.Params.CIPPEndpoint Write-LogMessage -headers $Request.Headers -API $APINAME-message 'Accessed this API' -Sev 'Debug' - + $Username = $request.body.userID $Tenantfilter = $request.body.tenantfilter $Permissions = $request.body.permissions if ($username -eq $null) { exit } - + $userid = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$($username)" -tenantid $Tenantfilter).id $Results = [System.Collections.ArrayList]::new() @@ -33,10 +33,18 @@ Function Invoke-ExecModifyMBPerms { } foreach ($Permission in $Permissions) { - $PermissionLevel = $Permission.PermissionLevel + $PermissionLevels = $Permission.PermissionLevel $Modification = $Permission.Modification $AutoMap = if ($Permission.PSObject.Properties.Name -contains 'AutoMap') { $Permission.AutoMap } else { $true } - + + # Handle multiple permission levels separated by commas + if ($PermissionLevels -like "*,*") { + $PermissionLevelArray = $PermissionLevels -split ',' | ForEach-Object { $_.Trim() } + } + else { + $PermissionLevelArray = @($PermissionLevels.Trim()) + } + # Handle UserID as array of objects or single value $TargetUsers = if ($Permission.UserID -is [array]) { $Permission.UserID | ForEach-Object { $_.value } @@ -46,79 +54,136 @@ Function Invoke-ExecModifyMBPerms { } foreach ($TargetUser in $TargetUsers) { - try { - switch ($PermissionLevel) { - 'FullAccess' { - if ($Modification -eq 'Remove') { - $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Remove-mailboxpermission' -cmdParams @{ - Identity = $userid - user = $TargetUser - accessRights = @('FullAccess') - Confirm = $false + foreach ($PermissionLevel in $PermissionLevelArray) { + try { + switch ($PermissionLevel) { + 'FullAccess' { + if ($Modification -eq 'Remove') { + $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Remove-mailboxpermission' -cmdParams @{ + Identity = $userid + user = $TargetUser + accessRights = @('FullAccess') + Confirm = $false + } + $null = $results.Add("Removed $($TargetUser) from $($username) Shared Mailbox permissions (FullAccess)") + } + else { + $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Add-MailboxPermission' -cmdParams @{ + Identity = $userid + user = $TargetUser + accessRights = @('FullAccess') + automapping = $AutoMap + Confirm = $false + } + $null = $results.Add("Granted $($TargetUser) access to $($username) Mailbox (FullAccess) with automapping set to $($AutoMap)") } - $null = $results.Add("Removed $($TargetUser) from $($username) Shared Mailbox permissions") } - else { - $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Add-MailboxPermission' -cmdParams @{ - Identity = $userid - user = $TargetUser - accessRights = @('FullAccess') - automapping = $AutoMap - Confirm = $false + 'SendAs' { + if ($Modification -eq 'Remove') { + $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Remove-RecipientPermission' -cmdParams @{ + Identity = $userid + Trustee = $TargetUser + accessRights = @('SendAs') + Confirm = $false + } + $null = $results.Add("Removed $($TargetUser) from $($username) with Send As permissions") + } + else { + $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Add-RecipientPermission' -cmdParams @{ + Identity = $userid + Trustee = $TargetUser + accessRights = @('SendAs') + Confirm = $false + } + $null = $results.Add("Granted $($TargetUser) access to $($username) with Send As permissions") } - $null = $results.Add("Granted $($TargetUser) access to $($username) Mailbox with automapping set to $($AutoMap)") } - } - 'SendAs' { - if ($Modification -eq 'Remove') { - $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Remove-RecipientPermission' -cmdParams @{ - Identity = $userid - Trustee = $TargetUser - accessRights = @('SendAs') - Confirm = $false + 'SendOnBehalf' { + if ($Modification -eq 'Remove') { + $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Set-Mailbox' -cmdParams @{ + Identity = $userid + GrantSendonBehalfTo = @{ + '@odata.type' = '#Exchange.GenericHashTable' + remove = $TargetUser + } + Confirm = $false + } + $null = $results.Add("Removed $($TargetUser) from $($username) Send on Behalf Permissions") + } + else { + $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Set-Mailbox' -cmdParams @{ + Identity = $userid + GrantSendonBehalfTo = @{ + '@odata.type' = '#Exchange.GenericHashTable' + add = $TargetUser + } + Confirm = $false + } + $null = $results.Add("Granted $($TargetUser) access to $($username) with Send On Behalf Permissions") } - $null = $results.Add("Removed $($TargetUser) from $($username) with Send As permissions") } - else { - $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Add-RecipientPermission' -cmdParams @{ - Identity = $userid - Trustee = $TargetUser - accessRights = @('SendAs') - Confirm = $false + 'ReadPermission' { + if ($Modification -eq 'Remove') { + $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Remove-MailboxPermission' -cmdParams @{ + Identity = $userid + user = $TargetUser + accessRights = @('ReadPermission') + Confirm = $false + } + $null = $results.Add("Removed $($TargetUser) from $($username) Read Permissions") } - $null = $results.Add("Granted $($TargetUser) access to $($username) with Send As permissions") } - } - 'SendOnBehalf' { - if ($Modification -eq 'Remove') { - $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Set-Mailbox' -cmdParams @{ - Identity = $userid - GrantSendonBehalfTo = @{ - '@odata.type' = '#Exchange.GenericHashTable' - remove = $TargetUser + 'ExternalAccount' { + if ($Modification -eq 'Remove') { + $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Remove-MailboxPermission' -cmdParams @{ + Identity = $userid + user = $TargetUser + accessRights = @('ExternalAccount') + Confirm = $false } - Confirm = $false + $null = $results.Add("Removed $($TargetUser) from $($username) Read Permissions") } - $null = $results.Add("Removed $($TargetUser) from $($username) Send on Behalf Permissions") } - else { - $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Set-Mailbox' -cmdParams @{ - Identity = $userid - GrantSendonBehalfTo = @{ - '@odata.type' = '#Exchange.GenericHashTable' - add = $TargetUser + 'DeleteItem' { + if ($Modification -eq 'Remove') { + $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Remove-MailboxPermission' -cmdParams @{ + Identity = $userid + user = $TargetUser + accessRights = @('DeleteItem') + Confirm = $false } - Confirm = $false + $null = $results.Add("Removed $($TargetUser) from $($username) Read Permissions") + } + } + 'ChangePermission' { + if ($Modification -eq 'Remove') { + $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Remove-MailboxPermission' -cmdParams @{ + Identity = $userid + user = $TargetUser + accessRights = @('ChangePermission') + Confirm = $false + } + $null = $results.Add("Removed $($TargetUser) from $($username) Read Permissions") + } + } + 'ChangeOwner' { + if ($Modification -eq 'Remove') { + $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Remove-MailboxPermission' -cmdParams @{ + Identity = $userid + user = $TargetUser + accessRights = @('ChangeOwner') + Confirm = $false + } + $null = $results.Add("Removed $($TargetUser) from $($username) Read Permissions") } - $null = $results.Add("Granted $($TargetUser) access to $($username) with Send On Behalf Permissions") } } + Write-LogMessage -headers $Request.Headers -API $APINAME-message "Executed $($PermissionLevel) permission modification for $($TargetUser) on $($username)" -Sev 'Info' -tenant $TenantFilter + } + catch { + Write-LogMessage -headers $Request.Headers -API $APINAME-message "Could not execute $($PermissionLevel) permission modification for $($TargetUser) on $($username)" -Sev 'Error' -tenant $TenantFilter + $null = $results.Add("Could not execute $($PermissionLevel) permission modification for $($TargetUser) on $($username). Error: $($_.Exception.Message)") } - Write-LogMessage -headers $Request.Headers -API $APINAME-message "Executed $($PermissionLevel) permission modification for $($TargetUser) on $($username)" -Sev 'Info' -tenant $TenantFilter - } - catch { - Write-LogMessage -headers $Request.Headers -API $APINAME-message "Could not execute $($PermissionLevel) permission modification for $($TargetUser) on $($username)" -Sev 'Error' -tenant $TenantFilter - $null = $results.Add("Could not execute $($PermissionLevel) permission modification for $($TargetUser) on $($username). Error: $($_.Exception.Message)") } } } From 0a21b10421299a6985645366e89442b387e4f631 Mon Sep 17 00:00:00 2001 From: Luke Steward <87503131+sfaxluke@users.noreply.github.com> Date: Fri, 6 Jun 2025 14:00:18 +0100 Subject: [PATCH 046/160] Removed $APINAME (1/3) --- .../Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 | 2 -- 1 file changed, 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 index 2bf8af08593e..3220da6c44f0 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 @@ -33,8 +33,6 @@ function Invoke-CIPPStandardConditionalAccessTemplate { If ($Settings.remediate -eq $true) { - $APINAME = 'Standards' - foreach ($Setting in $Settings) { try { From 7a4f910a03d430cbcb80933bc51734427a41eb59 Mon Sep 17 00:00:00 2001 From: Luke Steward <87503131+sfaxluke@users.noreply.github.com> Date: Fri, 6 Jun 2025 14:01:16 +0100 Subject: [PATCH 047/160] Removed $APINAME (2/3) --- .../Public/Standards/Invoke-CIPPStandardExConnector.ps1 | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExConnector.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExConnector.ps1 index 37910f14272d..d32d5008f175 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExConnector.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExConnector.ps1 @@ -8,7 +8,6 @@ function Invoke-CIPPStandardExConnector { If ($Settings.remediate -eq $true) { - $APINAME = 'Standards' foreach ($Template in $Settings.TemplateList) { try { $Table = Get-CippTable -tablename 'templates' @@ -20,10 +19,10 @@ function Invoke-CIPPStandardExConnector { if ($Existing) { $RequestParams | Add-Member -NotePropertyValue $Existing.Identity -NotePropertyName Identity -Force $null = New-ExoRequest -tenantid $Tenant -cmdlet "Set-$($ConnectorType)connector" -cmdParams $RequestParams -useSystemMailbox $true - Write-LogMessage -API $APINAME -tenant $Tenant -message "Updated transport rule for $($Tenant, $Settings)" -sev info + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Updated transport rule for $($Tenant, $Settings)" -sev info } else { $null = New-ExoRequest -tenantid $Tenant -cmdlet "New-$($ConnectorType)connector" -cmdParams $RequestParams -useSystemMailbox $true - Write-LogMessage -API $APINAME -tenant $Tenant -message "Created transport rule for $($Tenant, $Settings)" -sev info + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Created transport rule for $($Tenant, $Settings)" -sev info } } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message From 9f7488b85119d27ca86fa6586b1c839bc83e4cdd Mon Sep 17 00:00:00 2001 From: Luke Steward <87503131+sfaxluke@users.noreply.github.com> Date: Fri, 6 Jun 2025 14:02:04 +0100 Subject: [PATCH 048/160] Add log message endpoint for Transport Standards The variable was missing for $APIName however other standards no longer use the variable to define the log message location --- .../Standards/Invoke-CIPPStandardTransportRuleTemplate.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTransportRuleTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTransportRuleTemplate.ps1 index b867b66a54de..79e154dbe8e0 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTransportRuleTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTransportRuleTemplate.ps1 @@ -51,7 +51,7 @@ function Invoke-CIPPStandardTransportRuleTemplate { Write-LogMessage -API 'Standards' -tenant $tenant -message "Successfully created transport rule for $tenant" -sev 'Info' } - Write-LogMessage -API $APINAME -tenant $Tenant -message "Created transport rule for $($tenantFilter)" -sev 'Debug' + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Created transport rule for $($tenantFilter)" -sev 'Debug' } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -tenant $tenant -message "Could not create transport rule for $($tenantFilter): $ErrorMessage" -sev 'Error' From eaba7ab38d7a04c4ef55090d321fb2c01daa770e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Fri, 6 Jun 2025 16:47:59 +0200 Subject: [PATCH 049/160] Feat: add standard to restrict third-party storage services in Microsoft 365 --- ...ndardRestrictThirdPartyStorageServices.ps1 | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRestrictThirdPartyStorageServices.ps1 diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRestrictThirdPartyStorageServices.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRestrictThirdPartyStorageServices.ps1 new file mode 100644 index 000000000000..8da44190ffac --- /dev/null +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRestrictThirdPartyStorageServices.ps1 @@ -0,0 +1,93 @@ +function Invoke-CIPPStandardRestrictThirdPartyStorageServices { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) RestrictThirdPartyStorageServices + .SYNOPSIS + (Label) Restrict Third-Party Storage Services in Microsoft 365 on the web + .DESCRIPTION + (Helptext) Ensures that third-party storage services are restricted in Microsoft 365 on the web. This disables the ability for users to connect external storage providers like Dropbox, Google Drive, etc. through the Office 365 web interface. + (DocsDescription) Ensures that third-party storage services are restricted in Microsoft 365 on the web. This disables the ability for users to connect external storage providers like Dropbox, Google Drive, etc. through the Office 365 web interface by disabling the Microsoft 365 on the web service principal. + .NOTES + CAT + Global Standards + TAG + "CIS" + ADDEDCOMPONENT + IMPACT + Medium Impact + ADDEDDATE + 2025-06-06 + POWERSHELLEQUIVALENT + Get-AzureADServicePrincipal -Filter "appId eq 'c1f33bc0-bdb4-4248-ba9b-096807ddb43e'" | Set-AzureADServicePrincipal -AccountEnabled \$false + RECOMMENDEDBY + "CIS" + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + .LINK + https://docs.cipp.app/user-documentation/tenant/standards/list-standards + #> + + param ($Tenant, $Settings) + ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'RestrictThirdPartyStorageServices' + + $AppId = 'c1f33bc0-bdb4-4248-ba9b-096807ddb43e' + $Uri = "https://graph.microsoft.com/beta/servicePrincipals?`$filter=appId eq '$AppId'" + + try { + $CurrentState = New-GraphGetRequest -Uri $Uri -tenantid $Tenant | Select-Object displayName, accountEnabled, appId + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Could not get current state for Microsoft 365 on the web service principal. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Return + } + + if ($Settings.remediate -eq $true) { + Write-Host 'Time to remediate third-party storage services restriction' + + # Check if service principal is already disabled + if ($CurrentState.accountEnabled -eq $false) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Third-party storage services are already restricted (service principal is disabled).' -sev Info + } else { + # Disable the service principal to restrict third-party storage services + try { + $DisableBody = @{ + accountEnabled = $false + } | ConvertTo-Json -Depth 10 -Compress + + # Normal /servicePrincipal/AppId does not find the service principal, so gotta use the Upsert method. Also handles if the service principal does not exist nicely. + # https://learn.microsoft.com/en-us/graph/api/serviceprincipal-upsert?view=graph-rest-beta&tabs=http + $UpdateUri = "https://graph.microsoft.com/beta/servicePrincipals(appId='$AppId')" + $null = New-GraphPostRequest -Uri $UpdateUri -Body $DisableBody -TenantID $Tenant -Type PATCH + + # Refresh the current state after disabling + $CurrentState = New-GraphGetRequest -Uri $Uri -tenantid $Tenant | Select-Object displayName, accountEnabled, appId + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Successfully restricted third-party storage services in Microsoft 365 on the web.' -sev Info + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to restrict third-party storage services. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + } + } + } + + if ($Settings.alert -eq $true) { + if ($CurrentState.accountEnabled -eq $false) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Third-party storage services are restricted (service principal is disabled).' -sev Info + } else { + Write-StandardsAlert -message 'Third-party storage services are not restricted in Microsoft 365 on the web' -object $CurrentState -tenant $Tenant -standardName 'RestrictThirdPartyStorageServices' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Third-party storage services are not restricted.' -sev Info + } + } + + if ($Settings.report -eq $true) { + if ($null -eq $CurrentState.accountEnabled -or $CurrentState.accountEnabled -eq $true) { + Set-CIPPStandardsCompareField -FieldName 'standards.RestrictThirdPartyStorageServices' -FieldValue $false -Tenant $Tenant + Add-CIPPBPAField -FieldName 'ThirdPartyStorageServicesRestricted' -FieldValue $false -StoreAs bool -Tenant $Tenant + } else { + $CorrectState = $CurrentState.accountEnabled -eq $false ? $true : $false + Set-CIPPStandardsCompareField -FieldName 'standards.RestrictThirdPartyStorageServices' -FieldValue $CorrectState -Tenant $Tenant + Add-CIPPBPAField -FieldName 'ThirdPartyStorageServicesRestricted' -FieldValue $CorrectState -StoreAs bool -Tenant $Tenant + } + } +} From 6b20e60496dd3aedb0d540a2b44d3377120aabfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Fri, 6 Jun 2025 20:12:28 +0200 Subject: [PATCH 050/160] Fix: parameter input error: |System.ArgumentException|Entered frequency is invalid and add a bit more logging --- ...IPPStandardGlobalQuarantineNotifications.ps1 | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGlobalQuarantineNotifications.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGlobalQuarantineNotifications.ps1 index 73a674953353..9f93f49538c5 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGlobalQuarantineNotifications.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGlobalQuarantineNotifications.ps1 @@ -31,7 +31,7 @@ function Invoke-CIPPStandardGlobalQuarantineNotifications { param ($Tenant, $Settings) ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'GlobalQuarantineNotifications' - $CurrentState = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-QuarantinePolicy' -cmdParams @{ QuarantinePolicyType = 'GlobalQuarantinePolicy' } + $CurrentState = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-QuarantinePolicy' -cmdParams @{ QuarantinePolicyType = 'GlobalQuarantinePolicy' } | Select-Object -ExcludeProperty '*data.type' # This might take the cake on ugly hacky stuff i've done, # but i just cant understand why the API returns the values it does and not a timespan like the equivalent powershell command does @@ -51,8 +51,8 @@ function Invoke-CIPPStandardGlobalQuarantineNotifications { try { $WantedState = [timespan]$NotificationInterval } catch { - $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - Write-LogMessage -API 'Standards' -tenant $Tenant -message "GlobalQuarantineNotifications: Invalid NotificationInterval parameter set. Error: $ErrorMessage" -sev Error + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "GlobalQuarantineNotifications: Invalid NotificationInterval parameter set. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage return } @@ -64,14 +64,14 @@ function Invoke-CIPPStandardGlobalQuarantineNotifications { } else { try { if ($CurrentState.Name -eq 'DefaultGlobalPolicy') { - $null = New-ExoRequest -tenantid $Tenant -cmdlet 'New-QuarantinePolicy' -cmdParams @{ Name = 'DefaultGlobalTag'; QuarantinePolicyType = 'GlobalQuarantinePolicy'; EndUserSpamNotificationFrequency = [string]$WantedState.TotalHours } + $null = New-ExoRequest -tenantid $Tenant -cmdlet 'New-QuarantinePolicy' -cmdParams @{ Name = 'DefaultGlobalTag'; QuarantinePolicyType = 'GlobalQuarantinePolicy'; EndUserSpamNotificationFrequency = [string]$WantedState } } else { $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-QuarantinePolicy' -cmdParams @{Identity = $CurrentState.Identity; EndUserSpamNotificationFrequency = [string]$WantedState } } Write-LogMessage -API 'Standards' -tenant $Tenant -message "Set Global Quarantine Notifications to $WantedState" -sev Info } catch { - $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to set Global Quarantine Notifications to $WantedState. Error: $ErrorMessage" -sev Error + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to set Global Quarantine Notifications to $WantedState. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage } } } @@ -81,14 +81,13 @@ function Invoke-CIPPStandardGlobalQuarantineNotifications { if ($CurrentState.EndUserSpamNotificationFrequency -eq $WantedState) { Write-LogMessage -API 'Standards' -tenant $Tenant -message "Global Quarantine Notifications are set to the desired value of $WantedState" -sev Info } else { - $Object = $CurrentState | Select-Object -Property * -ExcludeProperty '*@odata.type' - Write-StandardsAlert -message "Global Quarantine Notifications are not set to the desired value of $WantedState" -object $Object -tenant $Tenant -standardName 'GlobalQuarantineNotifications' -standardId $Settings.standardId + Write-StandardsAlert -message "Global Quarantine Notifications are not set to the desired value of $WantedState" -object $CurrentState -tenant $Tenant -standardName 'GlobalQuarantineNotifications' -standardId $Settings.standardId Write-LogMessage -API 'Standards' -tenant $Tenant -message "Global Quarantine Notifications are not set to the desired value of $WantedState" -sev Info } } if ($Settings.report -eq $true) { - $notificationInterval = @{ NotificationInterval = "$(($CurrentState.EndUserSpamNotificationFrequency).totalHours) hours" } + $notificationInterval = @{ NotificationInterval = "$(($CurrentState.EndUserSpamNotificationFrequency).TotalHours) hours" } $ReportState = $CurrentState.EndUserSpamNotificationFrequency -eq $WantedState ? $true : $notificationInterval Set-CIPPStandardsCompareField -FieldName 'standards.GlobalQuarantineNotifications' -FieldValue $ReportState -Tenant $Tenant Add-CIPPBPAField -FieldName 'GlobalQuarantineNotificationsSet' -FieldValue [string]$CurrentState.EndUserSpamNotificationFrequency -StoreAs string -Tenant $Tenant From b17b9ee97ca32241c7a74cc8f7875d21b117d5f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Fri, 6 Jun 2025 22:48:09 +0200 Subject: [PATCH 051/160] Feat: Add internal phishing protection for Microsoft Forms --- ...ke-CIPPStandardFormsPhishingProtection.ps1 | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardFormsPhishingProtection.ps1 diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardFormsPhishingProtection.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardFormsPhishingProtection.ps1 new file mode 100644 index 000000000000..cea83fe112cf --- /dev/null +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardFormsPhishingProtection.ps1 @@ -0,0 +1,83 @@ +function Invoke-CIPPStandardFormsPhishingProtection { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) FormsPhishingProtection + .SYNOPSIS + (Label) Ensure internal phishing protection for Forms is enabled + .DESCRIPTION + (Helptext) Enables internal phishing protection for Microsoft Forms to help prevent malicious forms from being created and shared within the organization. This feature scans forms created by internal users for potential phishing content and suspicious patterns. + (DocsDescription) Enables internal phishing protection for Microsoft Forms by setting the isInOrgFormsPhishingScanEnabled property to true. This security feature helps protect organizations from internal phishing attacks through Microsoft Forms by automatically scanning forms created by internal users for potential malicious content, suspicious links, and phishing patterns. When enabled, Forms will analyze form content and block or flag potentially dangerous forms before they can be shared within the organization. + .NOTES + CAT + Defender Standards + TAG + "CIS", "Security", "PhishingProtection" + ADDEDCOMPONENT + IMPACT + Low Impact + ADDEDDATE + 2025-01-27 + POWERSHELLEQUIVALENT + Set-FormsSettings -isInOrgFormsPhishingScanEnabled $true + RECOMMENDEDBY + "CIS" + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + .LINK + https://docs.cipp.app/user-documentation/tenant/standards/list-standards + #> + + param ($Tenant, $Settings) + ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'FormsPhishingProtection' + + $Uri = 'https://graph.microsoft.com/beta/admin/forms/settings' + + try { + $CurrentState = (New-GraphGetRequest -Uri $Uri -tenantid $Tenant).isInOrgFormsPhishingScanEnabled + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Could not get current Forms settings. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Return + } + + if ($Settings.remediate -eq $true) { + Write-Host 'Time to remediate Forms phishing protection' + + # Check if phishing protection is already enabled + if ($CurrentState -eq $true) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Forms internal phishing protection is already enabled.' -sev Info + } else { + # Enable Forms phishing protection + try { + $Body = @{ + isInOrgFormsPhishingScanEnabled = $true + } | ConvertTo-Json -Depth 10 -Compress + + $null = New-GraphPostRequest -Uri $Uri -Body $Body -TenantID $Tenant -Type PATCH + + # Refresh the current state after enabling + $CurrentState = New-GraphGetRequest -Uri $Uri -tenantid $Tenant + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Successfully enabled Forms internal phishing protection.' -sev Info + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to enable Forms internal phishing protection. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + } + } + } + + if ($Settings.alert -eq $true) { + if ($CurrentState -eq $true) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Forms internal phishing protection is enabled.' -sev Info + } else { + Write-StandardsAlert -message 'Forms internal phishing protection is not enabled' -object $CurrentState -tenant $Tenant -standardName 'FormsPhishingProtection' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Forms internal phishing protection is not enabled.' -sev Info + } + } + + if ($Settings.report -eq $true) { + Set-CIPPStandardsCompareField -FieldName 'standards.FormsPhishingProtection' -FieldValue $CurrentState -Tenant $Tenant + Add-CIPPBPAField -FieldName 'FormsPhishingProtection' -FieldValue $CurrentState -StoreAs bool -Tenant $Tenant + } +} From f52f0ef9b47b79f2b850ff813a52cbda958ddca5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Fri, 6 Jun 2025 22:48:52 +0200 Subject: [PATCH 052/160] Feat: Add OrgSettings-Forms.ReadWrite.All scopes and roles to SAMManifest.json --- Modules/CIPPCore/Public/SAMManifest.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Modules/CIPPCore/Public/SAMManifest.json b/Modules/CIPPCore/Public/SAMManifest.json index ff59fb4f7e32..510350f1d35b 100644 --- a/Modules/CIPPCore/Public/SAMManifest.json +++ b/Modules/CIPPCore/Public/SAMManifest.json @@ -558,6 +558,14 @@ { "id": "b7887744-6746-4312-813d-72daeaee7e2d", "type": "Scope" + }, + { + "id": "346c19ff-3fb2-4e81-87a0-bac9e33990c1", + "type": "Scope" + }, + { + "id": "2cb92fee-97a3-4034-8702-24a6f5d0d1e9", + "type": "Role" } ] }, From 61b117eaf742a3dad9ab9bebc9cf0568eab2c5d5 Mon Sep 17 00:00:00 2001 From: Jr7468 Date: Fri, 6 Jun 2025 22:41:24 +0100 Subject: [PATCH 053/160] Modified Invoke-AddGroup to support dynamic membership for M365 groups --- .../Administration/Groups/Invoke-AddGroup.ps1 | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-AddGroup.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-AddGroup.ps1 index 29c47135f18c..e17056f91419 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-AddGroup.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-AddGroup.ps1 @@ -32,22 +32,32 @@ function Invoke-AddGroup { } if ($GroupObject.membershipRules) { $BodyParams | Add-Member -NotePropertyName 'membershipRule' -NotePropertyValue ($GroupObject.membershipRules) - $BodyParams | Add-Member -NotePropertyName 'groupTypes' -NotePropertyValue @('DynamicMembership') $BodyParams | Add-Member -NotePropertyName 'membershipRuleProcessingState' -NotePropertyValue 'On' + if ($GroupObject.groupType -eq 'm365') { + $BodyParams | Add-Member -NotePropertyName 'groupTypes' -NotePropertyValue @('Unified', 'DynamicMembership') + $BodyParams.mailEnabled = $true + } + else { + $BodyParams | Add-Member -NotePropertyName 'groupTypes' -NotePropertyValue @('DynamicMembership') + } + # Skip adding static members if we're using dynamic membership + $SkipStaticMembers = $true } - if ($GroupObject.groupType -eq 'm365') { + elseif ($GroupObject.groupType -eq 'm365') { $BodyParams | Add-Member -NotePropertyName 'groupTypes' -NotePropertyValue @('Unified') + $BodyParams.mailEnabled = $true } if ($GroupObject.owners) { $BodyParams | Add-Member -NotePropertyName 'owners@odata.bind' -NotePropertyValue (($GroupObject.owners) | ForEach-Object { "https://graph.microsoft.com/v1.0/users/$($_.value)" }) $BodyParams.'owners@odata.bind' = @($BodyParams.'owners@odata.bind') } - if ($GroupObject.members) { + if ($GroupObject.members -and -not $SkipStaticMembers) { $BodyParams | Add-Member -NotePropertyName 'members@odata.bind' -NotePropertyValue (($GroupObject.members) | ForEach-Object { "https://graph.microsoft.com/v1.0/users/$($_.value)" }) $BodyParams.'members@odata.bind' = @($BodyParams.'members@odata.bind') } $GraphRequest = New-GraphPostRequest -uri 'https://graph.microsoft.com/beta/groups' -tenantid $tenant -type POST -body (ConvertTo-Json -InputObject $BodyParams -Depth 10) -Verbose - } else { + } + else { if ($GroupObject.groupType -eq 'dynamicDistribution') { $ExoParams = @{ Name = $GroupObject.displayName @@ -55,7 +65,8 @@ function Invoke-AddGroup { PrimarySmtpAddress = $Email } $GraphRequest = New-ExoRequest -tenantid $tenant -cmdlet 'New-DynamicDistributionGroup' -cmdParams $ExoParams - } else { + } + else { $ExoParams = @{ Name = $GroupObject.displayName Alias = $GroupObject.username @@ -77,7 +88,8 @@ function Invoke-AddGroup { "Successfully created group $($GroupObject.displayName) for $($tenant)" Write-LogMessage -headers $Request.Headers -API $APIName -tenant $tenant -message "Created group $($GroupObject.displayName) with id $($GraphRequest.id)" -Sev Info - } catch { + } + catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -headers $Request.Headers -API $APIName -tenant $tenant -message "Group creation API failed. $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage "Failed to create group. $($GroupObject.displayName) for $($tenant) $($ErrorMessage.NormalizedError)" From 9e3bf5e0a78e0c570fcea44cc1a73e58a631b5dd Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 7 Jun 2025 16:44:55 -0400 Subject: [PATCH 054/160] reference definitions from frontend main repo --- .../Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 index 1603da37c897..245ff267103c 100644 --- a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 +++ b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 @@ -1816,7 +1816,7 @@ function Invoke-NinjaOneTenantSync { Set-Location $CIPPRoot try { - $StandardsDefinitions = Get-Content 'config/standards.json' | ConvertFrom-Json -Depth 100 + $StandardsDefinitions = Invoke-RestMethod -Uri 'https://raw.githubusercontent.com/KelvinTegelaar/CIPP/refs/heads/main/src/data/standards.json' $AppliedStandards = Get-CIPPStandards -TenantFilter $Customer.defaultDomainName $Templates = Get-CIPPTable 'templates' $StandardTemplates = Get-CIPPAzDataTableEntity @Templates | Where-Object { $_.PartitionKey -eq 'StandardsTemplateV2' } From da979c0e0c4320b7b62209cf143b5ce99a041e24 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 7 Jun 2025 17:31:43 -0400 Subject: [PATCH 055/160] fix mfa push --- .../Identity/Administration/Users/Invoke-ExecSendPush.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecSendPush.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecSendPush.ps1 index 78b099d6eee6..d9af4f6e232c 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecSendPush.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecSendPush.ps1 @@ -52,7 +52,7 @@ function Invoke-ExecSendPush { $SPBody = [pscustomobject]@{ appId = $MFAAppID - } + } | ConvertTo-Json -Depth 5 $SPID = (New-GraphPostRequest -uri 'https://graph.microsoft.com/v1.0/servicePrincipals' -tenantid $TenantFilter -type POST -body $SPBody -AsApp $true).id } From ce7781d95d4bd63fb616cba8f1177444271eb694 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 8 Jun 2025 17:12:26 +0200 Subject: [PATCH 056/160] fixes issues caused by incorrect grouptype --- .../Groups/Invoke-AddGroupTemplate.ps1 | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-AddGroupTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-AddGroupTemplate.ps1 index 12925c3fafbf..6b09fe1f3cb9 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-AddGroupTemplate.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-AddGroupTemplate.ps1 @@ -16,11 +16,20 @@ function Invoke-AddGroupTemplate { $GUID = $Request.Body.GUID ?? (New-Guid).GUID try { if (!$Request.Body.displayname) { throw 'You must enter a displayname' } - + $groupType = switch -wildcard ($Request.Body.groupType) { + '*dynamic*' { 'dynamic' } + '*azurerole*' { 'azurerole' } + '*unified*' { 'm365' } + '*Microsoft*' { 'm365' } + '*generic*' { 'generic' } + '*mail*' { 'mailenabledsecurity' } + '*Distribution*' { 'distribution' } + default { $Request.Body.groupType } + } $object = [PSCustomObject]@{ displayName = $Request.Body.displayName description = $Request.Body.description - groupType = $Request.Body.groupType + groupType = $groupType membershipRules = $Request.Body.membershipRules allowExternal = $Request.Body.allowExternal username = $Request.Body.username From 24149062cc5436e044d2525ce97268a2579d9056 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 8 Jun 2025 21:41:01 +0200 Subject: [PATCH 057/160] fixes deployment issues --- .../Identity/Administration/Groups/Invoke-AddGroupTemplate.ps1 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-AddGroupTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-AddGroupTemplate.ps1 index 6b09fe1f3cb9..51380b38dc68 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-AddGroupTemplate.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-AddGroupTemplate.ps1 @@ -24,8 +24,10 @@ function Invoke-AddGroupTemplate { '*generic*' { 'generic' } '*mail*' { 'mailenabledsecurity' } '*Distribution*' { 'distribution' } + '*security*' { 'security' } default { $Request.Body.groupType } } + if ($Request.body.membershipRules) { $groupType = 'dynamic' } $object = [PSCustomObject]@{ displayName = $Request.Body.displayName description = $Request.Body.description From e0de70cf4175537b326f8ced080a15197a60b006 Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Mon, 9 Jun 2025 12:37:46 +0800 Subject: [PATCH 058/160] corrected input value from alert configuration --- .../Public/Alerts/Get-CIPPAlertHuntressRogueApps.ps1 | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertHuntressRogueApps.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertHuntressRogueApps.ps1 index 0efe50733dc5..a949986da6a3 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertHuntressRogueApps.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertHuntressRogueApps.ps1 @@ -13,10 +13,8 @@ function Get-CIPPAlertHuntressRogueApps { Param ( [Parameter(Mandatory = $false)] [Alias('input')] - $InputValue, - $TenantFilter, - [Parameter(Mandatory = $false)] - [bool]$IgnoreDisabledApps = $false + [bool]$InputValue = $false, + $TenantFilter ) try { @@ -24,7 +22,7 @@ function Get-CIPPAlertHuntressRogueApps { $RogueAppFilter = $RogueApps.appId -join "','" $ServicePrincipals = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/servicePrincipals?`$filter=appId in ('$RogueAppFilter')" -tenantid $TenantFilter # If IgnoreDisabledApps is true, filter out disabled service principals - if ($IgnoreDisabledApps) { + if ($InputValue) { $ServicePrincipals = $ServicePrincipals | Where-Object { $_.accountEnabled -eq $true } } From 427116888a7271f7bb3986e3e51c4263d5e84688 Mon Sep 17 00:00:00 2001 From: ngms-psh Date: Mon, 9 Jun 2025 13:31:06 +0200 Subject: [PATCH 059/160] Added script alert for OneDrive usage quota --- .../Alerts/Get-CIPPAlertOnedriveQuota.ps1 | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 Modules/CIPPCore/Public/Alerts/Get-CIPPAlertOnedriveQuota.ps1 diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertOnedriveQuota.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertOnedriveQuota.ps1 new file mode 100644 index 000000000000..f0bff82aecfb --- /dev/null +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertOnedriveQuota.ps1 @@ -0,0 +1,46 @@ +function Get-CIPPAlertOneDriveQuota { + <# + .FUNCTIONALITY + Entrypoint + #> + [CmdletBinding()] + Param ( + [Parameter(Mandatory)] + $TenantFilter, + [Alias('input')] + [ValidateRange(0,100)] + [int]$InputValue = 90 + ) + + try { + $Usage = New-GraphGetRequest -tenantid $TenantFilter -uri "https://graph.microsoft.com/beta/reports/getOneDriveUsageAccountDetail(period='D7')?`$format=application/json&`$top=999" -AsApp $true + if (!$Usage) { + Write-AlertMessage -tenant $($TenantFilter) -message "OneDrive quota Alert: Unable to get OneDrive usage: Error occurred: No data returned from API." + return + } + } + catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-AlertMessage -tenant $($TenantFilter) -message "OneDrive quota Alert: Unable to get OneDrive usage: Error occurred: $ErrorMessage" + return + } + + #Check if the OneDrive quota is over the threshold + $OverQuota = $Usage | ForEach-Object { + if ($_.StorageUsedInBytes -eq 0 -or $_.storageAllocatedInBytes -eq 0) { return } + try { + $UsagePercent = [math]::Round(($_.storageUsedInBytes / $_.storageAllocatedInBytes) * 100) + } catch { $UsagePercent = 100 } + + if ($UsagePercent -gt $InputValue) { + $GBLeft = [math]::Round(($_.storageAllocatedInBytes - $_.storageUsedInBytes) / 1GB) + "$($_.ownerPrincipalName): OneDrive is $UsagePercent% full. OneDrive has $($GBLeft)GB storage left" + } + + } + + #If the quota is over the threshold, send an alert + if ($OverQuota) { + Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $OverQuota + } +} From cd61d80009ec335f0e74cded096eef5e0ec53462 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Mon, 9 Jun 2025 14:36:30 +0200 Subject: [PATCH 060/160] more words --- cspell.json | 141 +++++++++++++++++++++++++++------------------------- 1 file changed, 73 insertions(+), 68 deletions(-) diff --git a/cspell.json b/cspell.json index a46f912f562a..1bb15e727bfa 100644 --- a/cspell.json +++ b/cspell.json @@ -1,70 +1,75 @@ { - "version": "0.2", - "ignorePaths": [], - "dictionaryDefinitions": [], - "dictionaries": [], - "words": [ - "ADMS", - "AITM", - "Autotask", - "Bluetrait", - "CIPP", - "CIPP-API", - "Connectwise", - "Datto", - "Entra", - "GDAP", - "Intune", - "OBEE", - "Passwordless", - "PSTN", - "Sherweb", - "SSPR", - "Terrl", - "TNEF", - "winmail", - "Yubikey" - ], - "ignoreWords": [ - "tenantid", - "APINAME", - "CIPPBPA", - "CIPPCA", - "CIPPSPO", - "CIPPAPI", - "Addins", - "Helptext", - "ADDEDCOMPONENT", - "ADDEDDATE", - "POWERSHELLEQUIVALENT", - "RECOMMENDEDBY", - "UPDATECOMMENTBLOCK", - "DISABLEDFEATURES", - "pscustomobject", - "microsoftonline", - "mdo_safeattachments", - "mdo_highconfidencespamaction", - "mdo_highconfidencephishaction", - "mdo_phisspamacation", - "mdo_spam_notifications_only_for_admins", - "mdo_antiphishingpolicies", - "mdo_phishthresholdlevel", - "mdo_autoforwardingmode", - "mdo_blockmailforward", - "mdo_zapspam", - "mdo_zapphish", - "mdo_zapmalware", - "mdo_safedocuments", - "mdo_commonattachmentsfilter", - "mdo_safeattachmentpolicy", - "mdo_safelinksforemail", - "mdo_safelinksforOfficeApps", - "exo_storageproviderrestricted", - "exo_individualsharing", - "exo_outlookaddins", - "exo_mailboxaudit", - "exo_mailtipsenabled", - "mip_search_auditlog" - ], - "import": [] + "version": "0.2", + "ignorePaths": [], + "dictionaryDefinitions": [], + "dictionaries": [], + "words": [ + "ADMS", + "AITM", + "Autotask", + "Bluetrait", + "cipp", + "CIPP", + "CIPP-API", + "Connectwise", + "Datto", + "Entra", + "GDAP", + "Intune", + "OBEE", + "Passwordless", + "passwordless", + "PSTN", + "Sherweb", + "SSPR", + "Standardcal", + "Terrl", + "TNEF", + "winmail", + "Yubikey" + ], + "ignoreWords": [ + "tenantid", + "jnlp", + "APINAME", + "CIPPBPA", + "CIPPCA", + "CIPPSPO", + "CIPPAPI", + "donotchange", + "Addins", + "Helptext", + "ADDEDCOMPONENT", + "ADDEDDATE", + "POWERSHELLEQUIVALENT", + "RECOMMENDEDBY", + "UPDATECOMMENTBLOCK", + "DISABLEDFEATURES", + "pscustomobject", + "microsoftonline", + "mdo_safeattachments", + "mdo_highconfidencespamaction", + "mdo_highconfidencephishaction", + "mdo_phisspamacation", + "mdo_spam_notifications_only_for_admins", + "mdo_antiphishingpolicies", + "mdo_phishthresholdlevel", + "mdo_autoforwardingmode", + "mdo_blockmailforward", + "mdo_zapspam", + "mdo_zapphish", + "mdo_zapmalware", + "mdo_safedocuments", + "mdo_commonattachmentsfilter", + "mdo_safeattachmentpolicy", + "mdo_safelinksforemail", + "mdo_safelinksforOfficeApps", + "exo_storageproviderrestricted", + "exo_individualsharing", + "exo_outlookaddins", + "exo_mailboxaudit", + "exo_mailtipsenabled", + "mip_search_auditlog" + ], + "import": [] } From cf2536d69036fa6400804c52df1877a11758ade5 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 9 Jun 2025 15:36:13 -0400 Subject: [PATCH 061/160] fix tenant cleanup --- Modules/CIPPCore/Public/GraphHelper/Get-Tenants.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/GraphHelper/Get-Tenants.ps1 b/Modules/CIPPCore/Public/GraphHelper/Get-Tenants.ps1 index acdad77ac57a..c231eb916100 100644 --- a/Modules/CIPPCore/Public/GraphHelper/Get-Tenants.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/Get-Tenants.ps1 @@ -67,7 +67,7 @@ function Get-Tenants { relationshipEnd = $Relationship.endDateTime } } - $CurrentTenants = Get-CIPPAzDataTableEntity @TenantsTable -Filter "PartitionKey eq 'Tenants' and Excluded eq false" + $CurrentTenants = Get-CIPPAzDataTableEntity @TenantsTable -Filter "PartitionKey eq 'Tenants' and Excluded eq false and delegatedPrivilegeStatus ne 'directTenant'" $CurrentTenants | Where-Object { $_.customerId -notin $GDAPList.customerId -and $_.customerId -ne $env:TenantID } | ForEach-Object { Remove-AzDataTableEntity -Force @TenantsTable -Entity $_ } From 3264c872e8856a02c65bf58e844ca26084ebf164 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 10 Jun 2025 11:33:36 +0200 Subject: [PATCH 062/160] allow group editing --- .../Groups/Invoke-EditGroup.ps1 | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-EditGroup.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-EditGroup.ps1 index a6e660ad0320..4657b6b265fe 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-EditGroup.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-EditGroup.ps1 @@ -16,8 +16,7 @@ function Invoke-EditGroup { $UserObj = $Request.Body $GroupType = $UserObj.groupId.addedFields.groupType ? $UserObj.groupId.addedFields.groupType : $UserObj.groupType $GroupName = $UserObj.groupName ? $UserObj.groupName : $UserObj.groupId.addedFields.groupName - - #Write-Warning ($Request.Body | ConvertTo-Json -Depth 10) + $OrgGroup = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/groups/$($UserObj.groupId)" -tenantid $UserObj.tenantFilter $AddMembers = $UserObj.AddMember $UserObj.groupId = $UserObj.groupId.value ?? $UserObj.groupId @@ -30,6 +29,39 @@ function Invoke-EditGroup { $ExoBulkRequests = [System.Collections.Generic.List[object]]::new() $ExoLogs = [System.Collections.Generic.List[object]]::new() + #Edit properties: + if ($GroupType -eq 'Distribution List' -or $GroupType -eq 'Mail-Enabled Security') { + $Params = @{ Identity = $UserObj.groupId; DisplayName = $UserObj.displayName; Description = $UserObj.description; name = $UserObj.mailNickname } + $ExoBulkRequests.Add(@{ + CmdletInput = @{ + CmdletName = 'Set-DistributionGroup' + Parameters = $Params + } + }) + $ExoLogs.Add(@{ + message = "Success - Edited group properties for $($GroupName) group. It might take some time to reflect the changes." + target = $UserObj.groupId + }) + } else { + $PatchObj = @{ + displayName = $UserObj.displayName + description = $UserObj.description + mailNickname = $UserObj.mailNickname + mailEnabled = $OrgGroup.mailEnabled + securityEnabled = $OrgGroup.securityEnabled + } + Write-Host "body: $($PatchObj | ConvertTo-Json -Depth 10 -Compress)" -ForegroundColor Yellow + if ($UserObj.membershipRules) { $PatchObj | Add-Member -MemberType NoteProperty -Name 'membershipRule' -Value $UserObj.membershipRules -Force } + try { + $patch = New-GraphPOSTRequest -type PATCH -uri "https://graph.microsoft.com/beta/groups/$($UserObj.groupId)" -tenantid $UserObj.tenantFilter -body ($PatchObj | ConvertTo-Json -Depth 10 -Compress) + $Results.Add("Success - Edited group properties for $($GroupName) group. It might take some time to reflect the changes.") + Write-LogMessage -headers $Headers -API $APIName -tenant $UserObj.tenantFilter -message "Edited group properties for $($GroupName) group" -Sev 'Info' + } catch { + $Results.Add("Error - Failed to edit group properties: $($_.Exception.Message)") + Write-LogMessage -headers $Headers -API $APIName -tenant $UserObj.tenantFilter -message "Failed to patch group: $($_.Exception.Message)" -Sev 'Error' + } + } + if ($AddMembers) { $AddMembers | ForEach-Object { try { @@ -257,6 +289,8 @@ function Invoke-EditGroup { }) } + + Write-Information "Graph Bulk Requests: $($BulkRequests.Count)" if ($BulkRequests.Count -gt 0) { #Write-Warning 'EditUser - Executing Graph Bulk Requests' From a64063eca46352dd90aee0ae32544b4b0c3b8257 Mon Sep 17 00:00:00 2001 From: ngms-psh Date: Tue, 10 Jun 2025 15:49:07 +0200 Subject: [PATCH 063/160] If compare fails, catch and StateIsCorrect = false --- .../Invoke-CIPPStandardSpamFilterPolicy.ps1 | 79 ++++++++++--------- 1 file changed, 42 insertions(+), 37 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 index bbf74d0e09a4..c650c95ef858 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 @@ -75,43 +75,48 @@ function Invoke-CIPPStandardSpamFilterPolicy { $MarkAsSpamWebBugsInHtml = if ($Settings.MarkAsSpamWebBugsInHtml) { 'On' } else { 'Off' } $MarkAsSpamSensitiveWordList = if ($Settings.MarkAsSpamSensitiveWordList) { 'On' } else { 'Off' } - $StateIsCorrect = ($CurrentState.Name -eq $PolicyName) -and - ($CurrentState.SpamAction -eq $SpamAction) -and - ($CurrentState.SpamQuarantineTag -eq $SpamQuarantineTag) -and - ($CurrentState.HighConfidenceSpamAction -eq $HighConfidenceSpamAction) -and - ($CurrentState.HighConfidenceSpamQuarantineTag -eq $HighConfidenceSpamQuarantineTag) -and - ($CurrentState.BulkSpamAction -eq $BulkSpamAction) -and - ($CurrentState.BulkQuarantineTag -eq $BulkQuarantineTag) -and - ($CurrentState.PhishSpamAction -eq $PhishSpamAction) -and - ($CurrentState.PhishQuarantineTag -eq $PhishQuarantineTag) -and - ($CurrentState.HighConfidencePhishAction -eq 'Quarantine') -and - ($CurrentState.HighConfidencePhishQuarantineTag -eq $HighConfidencePhishQuarantineTag) -and - ($CurrentState.BulkThreshold -eq [int]$Settings.BulkThreshold) -and - ($CurrentState.QuarantineRetentionPeriod -eq 30) -and - ($CurrentState.IncreaseScoreWithImageLinks -eq $IncreaseScoreWithImageLinks) -and - ($CurrentState.IncreaseScoreWithNumericIps -eq 'On') -and - ($CurrentState.IncreaseScoreWithRedirectToOtherPort -eq 'On') -and - ($CurrentState.IncreaseScoreWithBizOrInfoUrls -eq $IncreaseScoreWithBizOrInfoUrls) -and - ($CurrentState.MarkAsSpamEmptyMessages -eq 'On') -and - ($CurrentState.MarkAsSpamJavaScriptInHtml -eq 'On') -and - ($CurrentState.MarkAsSpamFramesInHtml -eq $MarkAsSpamFramesInHtml) -and - ($CurrentState.MarkAsSpamObjectTagsInHtml -eq $MarkAsSpamObjectTagsInHtml) -and - ($CurrentState.MarkAsSpamEmbedTagsInHtml -eq $MarkAsSpamEmbedTagsInHtml) -and - ($CurrentState.MarkAsSpamFormTagsInHtml -eq $MarkAsSpamFormTagsInHtml) -and - ($CurrentState.MarkAsSpamWebBugsInHtml -eq $MarkAsSpamWebBugsInHtml) -and - ($CurrentState.MarkAsSpamSensitiveWordList -eq $MarkAsSpamSensitiveWordList) -and - ($CurrentState.MarkAsSpamSpfRecordHardFail -eq 'On') -and - ($CurrentState.MarkAsSpamFromAddressAuthFail -eq 'On') -and - ($CurrentState.MarkAsSpamNdrBackscatter -eq 'On') -and - ($CurrentState.MarkAsSpamBulkMail -eq 'On') -and - ($CurrentState.InlineSafetyTipsEnabled -eq $true) -and - ($CurrentState.PhishZapEnabled -eq $true) -and - ($CurrentState.SpamZapEnabled -eq $true) -and - ($CurrentState.EnableLanguageBlockList -eq $Settings.EnableLanguageBlockList) -and - ((-not $CurrentState.LanguageBlockList -and -not $Settings.LanguageBlockList.value) -or (!(Compare-Object -ReferenceObject $CurrentState.LanguageBlockList -DifferenceObject $Settings.LanguageBlockList.value))) -and - ($CurrentState.EnableRegionBlockList -eq $Settings.EnableRegionBlockList) -and - ((-not $CurrentState.RegionBlockList -and -not $Settings.RegionBlockList.value) -or (!(Compare-Object -ReferenceObject $CurrentState.RegionBlockList -DifferenceObject $Settings.RegionBlockList.value))) -and - (!(Compare-Object -ReferenceObject $CurrentState.AllowedSenderDomains -DifferenceObject ($Settings.AllowedSenderDomains.value ?? $Settings.AllowedSenderDomains))) + try { + $StateIsCorrect = ($CurrentState.Name -eq $PolicyName) -and + ($CurrentState.SpamAction -eq $SpamAction) -and + ($CurrentState.SpamQuarantineTag -eq $SpamQuarantineTag) -and + ($CurrentState.HighConfidenceSpamAction -eq $HighConfidenceSpamAction) -and + ($CurrentState.HighConfidenceSpamQuarantineTag -eq $HighConfidenceSpamQuarantineTag) -and + ($CurrentState.BulkSpamAction -eq $BulkSpamAction) -and + ($CurrentState.BulkQuarantineTag -eq $BulkQuarantineTag) -and + ($CurrentState.PhishSpamAction -eq $PhishSpamAction) -and + ($CurrentState.PhishQuarantineTag -eq $PhishQuarantineTag) -and + ($CurrentState.HighConfidencePhishAction -eq 'Quarantine') -and + ($CurrentState.HighConfidencePhishQuarantineTag -eq $HighConfidencePhishQuarantineTag) -and + ($CurrentState.BulkThreshold -eq [int]$Settings.BulkThreshold) -and + ($CurrentState.QuarantineRetentionPeriod -eq 30) -and + ($CurrentState.IncreaseScoreWithImageLinks -eq $IncreaseScoreWithImageLinks) -and + ($CurrentState.IncreaseScoreWithNumericIps -eq 'On') -and + ($CurrentState.IncreaseScoreWithRedirectToOtherPort -eq 'On') -and + ($CurrentState.IncreaseScoreWithBizOrInfoUrls -eq $IncreaseScoreWithBizOrInfoUrls) -and + ($CurrentState.MarkAsSpamEmptyMessages -eq 'On') -and + ($CurrentState.MarkAsSpamJavaScriptInHtml -eq 'On') -and + ($CurrentState.MarkAsSpamFramesInHtml -eq $MarkAsSpamFramesInHtml) -and + ($CurrentState.MarkAsSpamObjectTagsInHtml -eq $MarkAsSpamObjectTagsInHtml) -and + ($CurrentState.MarkAsSpamEmbedTagsInHtml -eq $MarkAsSpamEmbedTagsInHtml) -and + ($CurrentState.MarkAsSpamFormTagsInHtml -eq $MarkAsSpamFormTagsInHtml) -and + ($CurrentState.MarkAsSpamWebBugsInHtml -eq $MarkAsSpamWebBugsInHtml) -and + ($CurrentState.MarkAsSpamSensitiveWordList -eq $MarkAsSpamSensitiveWordList) -and + ($CurrentState.MarkAsSpamSpfRecordHardFail -eq 'On') -and + ($CurrentState.MarkAsSpamFromAddressAuthFail -eq 'On') -and + ($CurrentState.MarkAsSpamNdrBackscatter -eq 'On') -and + ($CurrentState.MarkAsSpamBulkMail -eq 'On') -and + ($CurrentState.InlineSafetyTipsEnabled -eq $true) -and + ($CurrentState.PhishZapEnabled -eq $true) -and + ($CurrentState.SpamZapEnabled -eq $true) -and + ($CurrentState.EnableLanguageBlockList -eq $Settings.EnableLanguageBlockList) -and + ((-not $CurrentState.LanguageBlockList -and -not $Settings.LanguageBlockList.value) -or (!(Compare-Object -ReferenceObject $CurrentState.LanguageBlockList -DifferenceObject $Settings.LanguageBlockList.value))) -and + ($CurrentState.EnableRegionBlockList -eq $Settings.EnableRegionBlockList) -and + ((-not $CurrentState.RegionBlockList -and -not $Settings.RegionBlockList.value) -or (!(Compare-Object -ReferenceObject $CurrentState.RegionBlockList -DifferenceObject $Settings.RegionBlockList.value))) -and + (!(Compare-Object -ReferenceObject $CurrentState.AllowedSenderDomains -DifferenceObject ($Settings.AllowedSenderDomains.value ?? $Settings.AllowedSenderDomains))) + } + catch { + $StateIsCorrect = $false + } $AcceptedDomains = New-ExoRequest -TenantId $Tenant -cmdlet 'Get-AcceptedDomain' From 26efa2f9c32169f510fb0392208cf4b3727e678f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Mon, 9 Jun 2025 22:43:39 +0200 Subject: [PATCH 064/160] += words --- cspell.json | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/cspell.json b/cspell.json index 1bb15e727bfa..0684ca9353ee 100644 --- a/cspell.json +++ b/cspell.json @@ -12,30 +12,47 @@ "CIPP", "CIPP-API", "Connectwise", + "CPIM", "Datto", "Entra", "GDAP", + "IMAP", "Intune", + "MAPI", "OBEE", - "Passwordless", "passwordless", + "Passwordless", "PSTN", "Sherweb", + "Signup", "SSPR", + "SharePoint", "Standardcal", "Terrl", "TNEF", + "weburl", "winmail", "Yubikey" ], "ignoreWords": [ + "ACOM", + "Sharepoint", "tenantid", "jnlp", + "wsfed", + "Imap", "APINAME", + "CIPPAPI", + "WCSS", + "cacheusers", "CIPPBPA", "CIPPCA", + "MCAPI", + "skuid", + "BPOS", + "EPMID", "CIPPSPO", - "CIPPAPI", + "CIPPTAP", "donotchange", "Addins", "Helptext", From f4609bb96a73a99eac4f3d282d5369a545018828 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Mon, 9 Jun 2025 22:43:58 +0200 Subject: [PATCH 065/160] remove unused old endpoints --- ...Invoke-ExecOffboard_Mailboxpermissions.ps1 | 17 ---------- .../Users/Invoke-ExecPerUserMFAAllUsers.ps1 | 34 ------------------- 2 files changed, 51 deletions(-) delete mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecOffboard_Mailboxpermissions.ps1 delete mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecPerUserMFAAllUsers.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecOffboard_Mailboxpermissions.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecOffboard_Mailboxpermissions.ps1 deleted file mode 100644 index 10b4c8576330..000000000000 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecOffboard_Mailboxpermissions.ps1 +++ /dev/null @@ -1,17 +0,0 @@ -using namespace System.Net - -Function Invoke-ExecOffboard_Mailboxpermissions { - <# - .FUNCTIONALITY - Entrypoint - .ROLE - Exchange.Mailbox.ReadWrite - #> - [CmdletBinding()] - param($Request, $TriggerMetadata) - - foreach ($Mailbox in $Mailboxes) { - Remove-CIPPMailboxPermissions -PermissionsLevel @('FullAccess', 'SendAs', 'SendOnBehalf') -userid $Mailbox.UserPrincipalName -AccessUser $QueueItem.User -TenantFilter $QueueItem.TenantFilter -APIName $APINAME -Headers $QueueItem.Headers - } - -} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecPerUserMFAAllUsers.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecPerUserMFAAllUsers.ps1 deleted file mode 100644 index 7a7c296b4016..000000000000 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecPerUserMFAAllUsers.ps1 +++ /dev/null @@ -1,34 +0,0 @@ -function Invoke-ExecPerUserMFAAllUsers { - <# - .FUNCTIONALITY - Entrypoint - - .ROLE - Identity.User.ReadWrite - #> - Param($Request, $TriggerMetadata) - - $APIName = $Request.Params.CIPPEndpoint - $Headers = $Request.Headers - Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' - - # XXX Seems to be an unused endpoint? - Bobby - - $TenantFilter = $request.Query.tenantFilter - $Users = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/users' -tenantid $TenantFilter - $Request = @{ - userId = $Users.id - TenantFilter = $TenantFilter - State = $Request.Query.State - Headers = $Request.Headers - APIName = $APIName - } - $Result = Set-CIPPPerUserMFA @Request - $Body = @{ - Results = @($Result) - } - Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ - StatusCode = [HttpStatusCode]::OK - Body = $Body - }) -} From d31956644dae65ad43a233226662113dae7b2b1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Mon, 9 Jun 2025 22:47:26 +0200 Subject: [PATCH 066/160] Refactor logging and fix some variable casing --- .../Users/Invoke-SetUserAliases.ps1 | 66 +++++++++---------- 1 file changed, 32 insertions(+), 34 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-SetUserAliases.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-SetUserAliases.ps1 index 3ca90f60b72a..d8f254cead0a 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-SetUserAliases.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-SetUserAliases.ps1 @@ -1,6 +1,6 @@ using namespace System.Net -Function Invoke-SetUserAliases { +Function Invoke-EditUserAliases { <# .FUNCTIONALITY Entrypoint @@ -12,9 +12,11 @@ Function Invoke-SetUserAliases { $APIName = $Request.Params.CIPPEndpoint $Headers = $Request.Headers - Write-LogMessage -headers $Headers -API $ApiName -message 'Accessed this API' -Sev 'Debug' + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' $UserObj = $Request.Body + $TenantFilter = $UserObj.tenantFilter + if ([string]::IsNullOrWhiteSpace($UserObj.id)) { $body = @{'Results' = @('Failed to manage aliases. No user ID provided') } Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ @@ -31,8 +33,8 @@ Function Invoke-SetUserAliases { try { if ($Aliases -or $RemoveAliases -or $UserObj.MakePrimary) { # Get current mailbox - $CurrentMailbox = New-ExoRequest -tenantid $UserObj.tenantFilter -cmdlet 'Get-Mailbox' -cmdParams @{ Identity = $UserObj.id } -UseSystemMailbox $true - + $CurrentMailbox = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-Mailbox' -cmdParams @{ Identity = $UserObj.id } -UseSystemMailbox $true + if (-not $CurrentMailbox) { throw 'Could not find mailbox for user' } @@ -40,26 +42,26 @@ Function Invoke-SetUserAliases { $CurrentProxyAddresses = @($CurrentMailbox.EmailAddresses) Write-Host "Current proxy addresses: $($CurrentProxyAddresses -join ', ')" $NewProxyAddresses = @($CurrentProxyAddresses) - + # Handle setting primary address if ($UserObj.MakePrimary) { $PrimaryAddress = $UserObj.MakePrimary Write-Host "Attempting to set primary address: $PrimaryAddress" - + # Normalize the primary address format if ($PrimaryAddress -notlike 'SMTP:*') { $PrimaryAddress = "SMTP:$($PrimaryAddress -replace '^smtp:', '')" } Write-Host "Normalized primary address: $PrimaryAddress" - + # Check if the address exists in the current addresses (case-insensitive) - $ExistingAddress = $CurrentProxyAddresses | Where-Object { + $ExistingAddress = $CurrentProxyAddresses | Where-Object { $current = $_.ToLower() $target = $PrimaryAddress.ToLower() Write-Host "Comparing: '$current' with '$target'" $current -eq $target } - + if (-not $ExistingAddress) { Write-Host "Available addresses: $($CurrentProxyAddresses -join ', ')" throw "Cannot set primary address. Address $($PrimaryAddress -replace '^SMTP:', '') not found in user's addresses." @@ -69,23 +71,22 @@ Function Invoke-SetUserAliases { $NewProxyAddresses = $NewProxyAddresses | ForEach-Object { if ($_ -like 'SMTP:*') { $_.ToLower() - } - else { + } else { $_ } } # Remove any existing version of the address (case-insensitive) - $NewProxyAddresses = $NewProxyAddresses | Where-Object { - $_.ToLower() -ne $PrimaryAddress.ToLower() + $NewProxyAddresses = $NewProxyAddresses | Where-Object { + $_.ToLower() -ne $PrimaryAddress.ToLower() } # Add the new primary address at the beginning $NewProxyAddresses = @($PrimaryAddress) + $NewProxyAddresses - - Write-LogMessage -API $ApiName -tenant ($UserObj.tenantFilter) -headers $Headers -message "Set primary address for $($CurrentMailbox.DisplayName)" -Sev Info - $null = $results.Add('Success. Set new primary address.') + + Write-LogMessage -API $APIName -tenant $TenantFilter -headers $Headers -message "Set primary address for $($CurrentMailbox.DisplayName)" -Sev Info + $Results.Add('Success. Set new primary address.') } - + # Remove specified aliases if ($RemoveAliases) { foreach ($Alias in $RemoveAliases) { @@ -94,12 +95,12 @@ Function Invoke-SetUserAliases { $Alias = "smtp:$Alias" } # Remove the alias case-insensitively - $NewProxyAddresses = $NewProxyAddresses | Where-Object { - $_.ToLower() -ne $Alias.ToLower() + $NewProxyAddresses = $NewProxyAddresses | Where-Object { + $_.ToLower() -ne $Alias.ToLower() } } - Write-LogMessage -API $ApiName -tenant ($UserObj.tenantFilter) -headers $Headers -message "Removed Aliases from $($CurrentMailbox.DisplayName)" -Sev Info - $null = $results.Add('Success. Removed specified aliases from user.') + Write-LogMessage -API $ApiName -tenant $TenantFilter -headers $Headers -message "Removed Aliases from $($CurrentMailbox.DisplayName)" -Sev Info + $Results.Add('Success. Removed specified aliases from user.') } # Add new aliases @@ -117,8 +118,8 @@ Function Invoke-SetUserAliases { } if ($AliasesToAdd.Count -gt 0) { $NewProxyAddresses = $NewProxyAddresses + $AliasesToAdd - Write-LogMessage -API $ApiName -tenant ($UserObj.tenantFilter) -headers $Headers -message "Added Aliases to $($CurrentMailbox.DisplayName)" -Sev Info - $null = $results.Add('Success. Added new aliases to user.') + Write-LogMessage -API $ApiName -tenant ($TenantFilter) -headers $Headers -message "Added Aliases to $($CurrentMailbox.DisplayName)" -Sev Info + $Results.Add('Success. Added new aliases to user.') } } @@ -127,21 +128,18 @@ Function Invoke-SetUserAliases { Identity = $UserObj.id EmailAddresses = $NewProxyAddresses } - $null = New-ExoRequest -tenantid $UserObj.tenantFilter -cmdlet 'Set-Mailbox' -cmdParams $Params -UseSystemMailbox $true + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Set-Mailbox' -cmdParams $Params -UseSystemMailbox $true + } else { + $Results.Add('No alias changes specified.') } - else { - $null = $results.Add('No alias changes specified.') - } - } - catch { + } catch { $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -API $ApiName -tenant ($UserObj.tenantFilter) -headers $Headers -message "Alias management failed. $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage - $null = $results.Add("Failed to manage aliases: $($ErrorMessage.NormalizedError)") + Write-LogMessage -API $ApiName -tenant ($TenantFilter) -headers $Headers -message "Alias management failed. $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage + $Results.Add("Failed to manage aliases: $($ErrorMessage.NormalizedError)") } - $body = @{'Results' = @($results) } Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::OK - Body = $Body + Body = @{'Results' = @($Results) } }) -} \ No newline at end of file +} From f72640700b30f68d1c031f51161bb1129f493acd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Mon, 9 Jun 2025 22:48:24 +0200 Subject: [PATCH 067/160] rename to fit function naming standards --- .../{Invoke-SetUserAliases.ps1 => Invoke-EditUserAliases.ps1} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/{Invoke-SetUserAliases.ps1 => Invoke-EditUserAliases.ps1} (100%) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-SetUserAliases.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditUserAliases.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-SetUserAliases.ps1 rename to Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditUserAliases.ps1 From 3fa17c27d279a91bb155e653f96312d847a3904a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Tue, 10 Jun 2025 00:27:15 +0200 Subject: [PATCH 068/160] refactor a few functions to add error handling lots of casing fixes mooooooore words --- .../CIPPCore/Public/Add-CIPPGroupMember.ps1 | 25 ++++--- .../Devices/Invoke-ExecDeviceDelete.ps1 | 15 ++-- .../Administration/Users/Invoke-AddGuest.ps1 | 22 +++--- .../Administration/Users/Invoke-AddUser.ps1 | 19 ++--- .../Users/Invoke-AddUserBulk.ps1 | 11 ++- .../Users/Invoke-CIPPOffboardingJob.ps1 | 4 +- .../Administration/Users/Invoke-EditUser.ps1 | 18 ++--- .../Users/Invoke-ExecBECCheck.ps1 | 4 +- .../Users/Invoke-ExecClrImmId.ps1 | 15 ++-- .../Users/Invoke-ExecCreateTAP.ps1 | 6 +- .../Users/Invoke-ExecDisableUser.ps1 | 3 +- .../Users/Invoke-ExecDismissRiskyUser.ps1 | 2 +- .../Users/Invoke-ExecJITAdmin.ps1 | 6 +- .../Users/Invoke-ExecOneDriveShortCut.ps1 | 11 +-- .../Users/Invoke-ExecOnedriveProvision.ps1 | 19 +++-- .../Users/Invoke-ExecResetMFA.ps1 | 6 +- .../Users/Invoke-ExecResetPass.ps1 | 7 +- .../Users/Invoke-ExecRestoreDeleted.ps1 | 3 +- .../Users/Invoke-ExecRevokeSessions.ps1 | 5 +- .../Users/Invoke-ListPerUserMFA.ps1 | 8 +- ...voke-ListUserConditionalAccessPolicies.ps1 | 11 ++- .../Users/Invoke-ListUserCounts.ps1 | 17 ++--- .../Users/Invoke-ListUserDevices.ps1 | 9 +-- .../Users/Invoke-ListUserMailboxDetails.ps1 | 4 +- .../Users/Invoke-ListUserMailboxRules.ps1 | 29 +++---- .../Users/Invoke-ListUserPhoto.ps1 | 7 +- .../Users/Invoke-ListUserSettings.ps1 | 5 +- .../Users/Invoke-ListUserSigninLogs.ps1 | 30 ++++---- .../Administration/Users/Invoke-ListUsers.ps1 | 4 +- .../Users/Invoke-RemoveDeletedObject.ps1 | 3 +- .../Teams-Sharepoint/Invoke-AddSite.ps1 | 18 +++-- .../Teams-Sharepoint/Invoke-AddSiteBulk.ps1 | 14 ++-- ...cRemoveTeamsVoicePhoneNumberAssignment.ps1 | 25 ++++--- .../Invoke-ExecSetSharePointMember.ps1 | 38 ++++++---- .../Invoke-ExecSharePointPerms.ps1 | 10 +-- .../Invoke-ListSharepointQuota.ps1 | 26 +++---- .../Invoke-ListSharepointSettings.ps1 | 12 +-- .../Teams-Sharepoint/Invoke-ListSites.ps1 | 6 +- .../Teams-Sharepoint/Invoke-ListTeams.ps1 | 5 +- .../Invoke-ListTeamsActivity.ps1 | 8 +- .../Administration/Alerts/Invoke-AddAlert.ps1 | 19 ++--- .../Alerts/Invoke-ExecAuditLogSearch.ps1 | 4 + .../Alerts/Invoke-ListAuditLogTest.ps1 | 4 + .../Alerts/Invoke-ListWebhookAlert.ps1 | 6 +- .../Alerts/Invoke-PublicWebhooks.ps1 | 22 +++--- .../Invoke-ExecAppApproval.ps1 | 6 +- .../Invoke-ExecAppApprovalTemplate.ps1 | 12 +-- .../Invoke-ExecAppPermissionTemplate.ps1 | 12 ++- .../Administration/Invoke-ExecAddSPN.ps1 | 15 ++-- .../Invoke-ExecOffboardTenant.ps1 | 75 ++++++++++--------- .../Invoke-ExecOnboardTenant.ps1 | 9 ++- .../Invoke-ExecUpdateSecureScore.ps1 | 23 ++++-- .../Invoke-ListAppConsentRequests.ps1 | 18 +++-- .../Administration/Invoke-ListDomains.ps1 | 14 ++-- .../Invoke-ListTenantOnboarding.ps1 | 2 +- .../Administration/Invoke-SetAuthMethod.ps1 | 6 +- .../Tenant/Invoke-AddTenant.ps1 | 5 +- .../Tenant/Invoke-EditTenant.ps1 | 4 +- .../Tenant/Invoke-ListTenantDetails.ps1 | 37 +++++---- .../Tenant/Invoke-ListTenants.ps1 | 12 +-- .../Public/Entrypoints/Invoke-ListCSPsku.ps1 | 4 +- .../CIPPCore/Public/Get-CIPPPerUserMFA.ps1 | 10 +-- .../Public/New-CIPPOneDriveShortCut.ps1 | 17 +++-- .../Public/New-CIPPSharepointSite.ps1 | 39 ++++++++-- .../Public/Remove-CIPPGroupMember.ps1 | 24 +++--- .../Public/Request-CIPPSPOPersonalSite.ps1 | 7 +- .../CIPPCore/Public/Revoke-CIPPSessions.ps1 | 6 +- .../CIPPCore/Public/Set-CIPPResetPassword.ps1 | 14 ++-- cspell.json | 9 ++- 69 files changed, 501 insertions(+), 424 deletions(-) diff --git a/Modules/CIPPCore/Public/Add-CIPPGroupMember.ps1 b/Modules/CIPPCore/Public/Add-CIPPGroupMember.ps1 index 455b2dcad9c4..1b6674d6e203 100644 --- a/Modules/CIPPCore/Public/Add-CIPPGroupMember.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPGroupMember.ps1 @@ -7,21 +7,22 @@ function Add-CIPPGroupMember( [string]$APIName = 'Add Group Member' ) { try { - if ($member -like '*#EXT#*') { $member = [System.Web.HttpUtility]::UrlEncode($member) } - $MemberIDs = 'https://graph.microsoft.com/v1.0/directoryObjects/' + (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$($member)" -tenantid $TenantFilter).id - $addmemberbody = "{ `"members@odata.bind`": $(ConvertTo-Json @($MemberIDs)) }" + if ($Member -like '*#EXT#*') { $Member = [System.Web.HttpUtility]::UrlEncode($Member) } + $MemberIDs = 'https://graph.microsoft.com/v1.0/directoryObjects/' + (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$($Member)" -tenantid $TenantFilter).id + $AddMemberBody = "{ `"members@odata.bind`": $(ConvertTo-Json @($MemberIDs)) }" if ($GroupType -eq 'Distribution list' -or $GroupType -eq 'Mail-Enabled Security') { - $Params = @{ Identity = $GroupId; Member = $member; BypassSecurityGroupManagerCheck = $true } - $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Add-DistributionGroupMember' -cmdParams $params -UseSystemMailbox $true + $Params = @{ Identity = $GroupId; Member = $Member; BypassSecurityGroupManagerCheck = $true } + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Add-DistributionGroupMember' -cmdParams $Params -UseSystemMailbox $true } else { - $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/groups/$($GroupId)" -tenantid $TenantFilter -type patch -body $addmemberbody -Verbose + $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/groups/$($GroupId)" -tenantid $TenantFilter -type patch -body $AddMemberBody -Verbose } - $Message = "Successfully added user $($Member) to $($GroupId)." - Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Message -Sev 'Info' - return $message + $Results = "Successfully added user $($Member) to $($GroupId)." + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Results -Sev 'Info' + return $Results } catch { - $message = "Failed to add user $($Member) to $($GroupId) - $($_.Exception.Message)" - Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $message -Sev 'error' -LogData (Get-CippException -Exception $_) - return $message + $ErrorMessage = Get-CippException -Exception $_ + $Results = "Failed to add user $($Member) to $($GroupId) - $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Results -Sev 'error' -LogData $ErrorMessage + throw $Results } } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Devices/Invoke-ExecDeviceDelete.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Devices/Invoke-ExecDeviceDelete.ps1 index 1916328d51da..8ca7b8fef050 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Devices/Invoke-ExecDeviceDelete.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Devices/Invoke-ExecDeviceDelete.ps1 @@ -12,28 +12,25 @@ Function Invoke-ExecDeviceDelete { $APIName = $Request.Params.CIPPEndpoint $Headers = $Request.Headers - Write-LogMessage -Headers $Headers -API $APINAME -message 'Accessed this API' -Sev 'Debug' + Write-LogMessage -Headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' # Interact with body parameters or the body of the request. - $TenantFilter = $Request.body.tenantFilter ?? $Request.Query.tenantFilter - $Action = $Request.body.action ?? $Request.Query.action - $DeviceID = $Request.body.ID ?? $Request.Query.ID + $TenantFilter = $Request.Body.tenantFilter ?? $Request.Query.tenantFilter + $Action = $Request.Body.action ?? $Request.Query.action + $DeviceID = $Request.Body.ID ?? $Request.Query.ID try { - $Results = Set-CIPPDeviceState -Action $Action -DeviceID $DeviceID -TenantFilter $TenantFilter -Headers $Request.Headers -APIName $APINAME + $Results = Set-CIPPDeviceState -Action $Action -DeviceID $DeviceID -TenantFilter $TenantFilter -Headers $Headers -APIName $APIName $StatusCode = [HttpStatusCode]::OK } catch { $Results = $_.Exception.Message $StatusCode = [HttpStatusCode]::BadRequest } - Write-Host $Results - $body = [pscustomobject]@{'Results' = "$Results" } - # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ StatusCode = $StatusCode - Body = $body + Body = @{ 'Results' = $Results } }) } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddGuest.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddGuest.ps1 index d84dd6bc8bcb..cc9db658bf78 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddGuest.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddGuest.ps1 @@ -15,47 +15,45 @@ Function Invoke-AddGuest { Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' $TenantFilter = $Request.Body.tenantFilter - - $Results = [System.Collections.ArrayList]@() $UserObject = $Request.Body + try { if ($UserObject.RedirectURL) { - $BodyToship = [pscustomobject] @{ + $BodyToShip = [pscustomobject] @{ 'InvitedUserDisplayName' = $UserObject.DisplayName 'InvitedUserEmailAddress' = $($UserObject.mail) 'inviteRedirectUrl' = $($UserObject.RedirectURL) 'sendInvitationMessage' = [bool]$UserObject.SendInvite } } else { - $BodyToship = [pscustomobject] @{ + $BodyToShip = [pscustomobject] @{ 'InvitedUserDisplayName' = $UserObject.DisplayName 'InvitedUserEmailAddress' = $($UserObject.mail) 'sendInvitationMessage' = [bool]$UserObject.SendInvite 'inviteRedirectUrl' = 'https://myapps.microsoft.com' } } - $bodyToShip = ConvertTo-Json -Depth 10 -InputObject $BodyToship -Compress - $null = New-GraphPostRequest -uri 'https://graph.microsoft.com/beta/invitations' -tenantid $TenantFilter -type POST -body $BodyToship -verbose + $bodyToShip = ConvertTo-Json -Depth 10 -InputObject $BodyToShip -Compress + $null = New-GraphPostRequest -uri 'https://graph.microsoft.com/beta/invitations' -tenantid $TenantFilter -type POST -body $BodyToShip -Verbose if ($UserObject.SendInvite -eq $true) { - $Results.Add('Invited Guest. Invite Email sent') - Write-LogMessage -headers $Headers -API $APIName -tenant $($TenantFilter) -message "Invited Guest $($UserObject.DisplayName) with Email Invite " -Sev 'Info' + $Result = "Invited Guest $($UserObject.DisplayName) with Email Invite" + Write-LogMessage -headers $Headers -API $APIName -tenant $($TenantFilter) -message $Result -Sev 'Info' } else { - $Results.Add('Invited Guest. No Invite Email was sent') - Write-LogMessage -headers $Headers -API $APIName -tenant $($TenantFilter) -message "Invited Guest $($UserObject.DisplayName) with no Email Invite " -Sev 'Info' + $Result = "Invited Guest $($UserObject.DisplayName) with no Email Invite" + Write-LogMessage -headers $Headers -API $APIName -tenant $($TenantFilter) -message $Result -Sev 'Info' } $StatusCode = [HttpStatusCode]::OK } catch { $ErrorMessage = Get-CippException -Exception $_ $Result = "Failed to Invite Guest. $($ErrorMessage.NormalizedError)" Write-LogMessage -headers $Headers -API $APIName -tenant $($TenantFilter) -message $Result -Sev 'Error' -LogData $ErrorMessage - $Results.Add($Result) $StatusCode = [HttpStatusCode]::BadRequest } # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ StatusCode = $StatusCode - Body = @{Results = @($Results) } + Body = @{ 'Results' = @($Result) } }) } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddUser.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddUser.ps1 index 019ab34d981f..f67050101183 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddUser.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddUser.ps1 @@ -10,20 +10,21 @@ Function Invoke-AddUser { [CmdletBinding()] param($Request, $TriggerMetadata) - $APIName = 'AddUser' - Write-LogMessage -headers $Request.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' - $UserObj = $Request.body + $UserObj = $Request.Body if ($UserObj.Scheduled.Enabled) { $TaskBody = [pscustomobject]@{ - TenantFilter = $UserObj.tenantfilter + TenantFilter = $UserObj.tenantFilter Name = "New user creation: $($UserObj.mailNickname)@$($UserObj.PrimDomain.value)" Command = @{ value = 'New-CIPPUserTask' label = 'New-CIPPUserTask' } - Parameters = [pscustomobject]@{ userobj = $UserObj } + Parameters = [pscustomobject]@{ UserObj = $UserObj } ScheduledTime = $UserObj.Scheduled.date PostExecution = @{ Webhook = [bool]$Request.Body.PostExecution.Webhook @@ -31,20 +32,20 @@ Function Invoke-AddUser { PSA = [bool]$Request.Body.PostExecution.PSA } } - Add-CIPPScheduledTask -Task $TaskBody -hidden $false -DisallowDuplicateName $true -Headers $Request.Headers + Add-CIPPScheduledTask -Task $TaskBody -hidden $false -DisallowDuplicateName $true -Headers $Headers $body = [pscustomobject] @{ 'Results' = @("Successfully created scheduled task to create user $($UserObj.DisplayName)") } } else { - $CreationResults = New-CIPPUserTask -userobj $UserObj -APIName $APINAME -Headers $Request.Headers + $CreationResults = New-CIPPUserTask -UserObj $UserObj -APIName $APIName -Headers $Headers $body = [pscustomobject] @{ 'Results' = @( $CreationResults.Results[0], $CreationResults.Results[1], @{ 'resultText' = $CreationResults.Results[2] - 'copyField' = $CreationResults.password - 'state' = 'success' + 'copyField' = $CreationResults.password + 'state' = 'success' } ) 'CopyFrom' = @{ diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddUserBulk.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddUserBulk.ps1 index 9a017652bd39..3bb2c6b06a53 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddUserBulk.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddUserBulk.ps1 @@ -10,9 +10,12 @@ function Invoke-AddUserBulk { [CmdletBinding()] param($Request, $TriggerMetadata) - $APIName = 'AddUserBulk' - Write-LogMessage -headers $Request.Headers -API $APINAME -message 'Accessed this API' -Sev 'Debug' - $TenantFilter = $Request.body.TenantFilter + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + + # Interact with body parameters or the body of the request. + $TenantFilter = $Request.Body.tenantFilter $BulkUsers = $Request.Body.BulkUser $AssignedLicenses = $Request.Body.licenses @@ -78,7 +81,7 @@ function Invoke-AddUserBulk { # Add all other properties foreach ($key in $User.PSObject.Properties.Name) { if ($key -notin @('displayName', 'mailNickName', 'domain', 'password', 'usageLocation', 'businessPhones')) { - if (![string]::IsNullOrEmpty($User.$key) -and $UserBody.$key -eq $null) { + if (![string]::IsNullOrEmpty($User.$key) -and $null -eq $UserBody.$key) { $UserBody.$key = $User.$key } } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-CIPPOffboardingJob.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-CIPPOffboardingJob.ps1 index 3bb2fc720957..50e985309f1a 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-CIPPOffboardingJob.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-CIPPOffboardingJob.ps1 @@ -27,7 +27,7 @@ function Invoke-CIPPOffboardingJob { Remove-CIPPGroups -userid $userid -tenantFilter $TenantFilter -Headers $Headers -APIName $APIName -Username "$Username" } { $_.HideFromGAL -eq $true } { - Set-CIPPHideFromGAL -tenantFilter $TenantFilter -UserID $username -hidefromgal $true -Headers $Headers -APIName $APIName + Set-CIPPHideFromGAL -tenantFilter $TenantFilter -UserID $username -HideFromGAL $true -Headers $Headers -APIName $APIName } { $_.DisableSignIn -eq $true } { Set-CIPPSignInState -TenantFilter $TenantFilter -userid $username -AccountEnabled $false -Headers $Headers -APIName $APIName @@ -99,7 +99,7 @@ function Invoke-CIPPOffboardingJob { Remove-CIPPUserMFA -UserPrincipalName $Username -TenantFilter $TenantFilter -Headers $Headers } { $_.'ClearImmutableId' -eq $true } { - Clear-CIPPImmutableId -userid $userid -TenantFilter $TenantFilter -Headers $Headers -APIName $APIName + Clear-CIPPImmutableID -UserID $userid -TenantFilter $TenantFilter -Headers $Headers -APIName $APIName } } return $Return diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditUser.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditUser.ps1 index 08969005537f..d40f20130797 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditUser.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditUser.ps1 @@ -69,18 +69,18 @@ function Invoke-EditUser { } $bodyToShip = ConvertTo-Json -Depth 10 -InputObject $BodyToship -Compress $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/users/$($UserObj.id)" -tenantid $UserObj.tenantFilter -type PATCH -body $BodyToship -verbose - $null = $Results.Add( 'Success. The user has been edited.' ) + $Results.Add( 'Success. The user has been edited.' ) Write-LogMessage -API $APIName -tenant ($UserObj.tenantFilter) -headers $Headers -message "Edited user $($UserObj.DisplayName) with id $($UserObj.id)" -Sev Info if ($UserObj.password) { $passwordProfile = [pscustomobject]@{'passwordProfile' = @{ 'password' = $UserObj.password; 'forceChangePasswordNextSignIn' = [boolean]$UserObj.MustChangePass } } | ConvertTo-Json - $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/users/$($UserObj.id)" -tenantid $UserObj.tenantFilter -type PATCH -body $PasswordProfile -verbose - $null = $Results.Add("Success. The password has been set to $($UserObj.password)") + $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/users/$($UserObj.id)" -tenantid $UserObj.tenantFilter -type PATCH -body $PasswordProfile -Verbose + $Results.Add("Success. The password has been set to $($UserObj.password)") Write-LogMessage -API $APIName -tenant ($UserObj.tenantFilter) -headers $Headers -message "Reset $($UserObj.DisplayName)'s Password" -Sev Info } } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API $APIName -tenant ($UserObj.tenantFilter) -headers $Headers -message "User edit API failed. $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage - $null = $Results.Add( "Failed to edit user. $($ErrorMessage.NormalizedError)") + $Results.Add( "Failed to edit user. $($ErrorMessage.NormalizedError)") } @@ -90,7 +90,7 @@ function Invoke-EditUser { if ($licenses -or $UserObj.removeLicenses) { if ($UserObj.sherwebLicense.value) { $null = Set-SherwebSubscription -Headers $Headers -TenantFilter $UserObj.tenantFilter -SKU $UserObj.sherwebLicense.value -Add 1 - $null = $Results.Add('Added Sherweb License, scheduling assignment') + $Results.Add('Added Sherweb License, scheduling assignment') $taskObject = [PSCustomObject]@{ TenantFilter = $UserObj.tenantFilter Name = "Assign License: $UserPrincipalName" @@ -115,16 +115,16 @@ function Invoke-EditUser { #if the list of skuIds in $CurrentLicenses.assignedLicenses is EXACTLY the same as $licenses, we don't need to do anything, but the order in both can be different. if (($CurrentLicenses.assignedLicenses.skuId -join ',') -eq ($licenses -join ',') -and $UserObj.removeLicenses -eq $false) { Write-Host "$($CurrentLicenses.assignedLicenses.skuId -join ',') $(($licenses -join ','))" - $null = $Results.Add( 'Success. User license is already correct.' ) + $Results.Add( 'Success. User license is already correct.' ) } else { if ($UserObj.removeLicenses) { $licResults = Set-CIPPUserLicense -UserId $UserObj.id -TenantFilter $UserObj.tenantFilter -RemoveLicenses $CurrentLicenses.assignedLicenses.skuId -Headers $Headers - $null = $Results.Add($licResults) + $Results.Add($licResults) } else { #Remove all objects from $CurrentLicenses.assignedLicenses.skuId that are in $licenses $RemoveLicenses = $CurrentLicenses.assignedLicenses.skuId | Where-Object { $_ -notin $licenses } $licResults = Set-CIPPUserLicense -UserId $UserObj.id -TenantFilter $UserObj.tenantFilter -RemoveLicenses $RemoveLicenses -AddLicenses $licenses -Headers $headers - $null = $Results.Add($licResults) + $Results.Add($licResults) } } @@ -134,7 +134,7 @@ function Invoke-EditUser { } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API $APIName -tenant ($UserObj.tenantFilter) -headers $Headers -message "License assign API failed. $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage - $null = $Results.Add( "We've failed to assign the license. $($ErrorMessage.NormalizedError)") + $Results.Add( "We've failed to assign the license. $($ErrorMessage.NormalizedError)") Write-Warning "License assign API failed. $($_.Exception.Message)" Write-Information $_.InvocationInfo.PositionMessage } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBECCheck.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBECCheck.ps1 index 27389fffd877..533e02eea899 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBECCheck.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBECCheck.ps1 @@ -19,7 +19,7 @@ Function Invoke-ExecBECCheck { $Batch = @{ 'FunctionName' = 'BECRun' 'UserID' = $Request.Query.userid - 'TenantFilter' = $Request.Query.tenantfilter + 'TenantFilter' = $Request.Query.tenantFilter 'userName' = $Request.Query.userName } @@ -40,7 +40,7 @@ Function Invoke-ExecBECCheck { SkipLog = $true } #Write-Host ($InputObject | ConvertTo-Json) - $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) + $null = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ( ConvertTo-Json -InputObject $InputObject -Depth 5 -Compress ) @{ GUID = $Request.Query.userid } } else { diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecClrImmId.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecClrImmId.ps1 index 50374bc33a1b..b234dfbbf136 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecClrImmId.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecClrImmId.ps1 @@ -11,23 +11,24 @@ Function Invoke-ExecClrImmId { param($Request, $TriggerMetadata) $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev Debug + + # Interact with body parameters or the body of the request. $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter - Write-LogMessage -headers $Request.Headers -API $APIName -message 'Accessed this API' -Sev Debug $UserID = $Request.Query.ID ?? $Request.Body.ID - Try { - $Result = Clear-CIPPImmutableId -userid $UserID -TenantFilter $TenantFilter -Headers $Request.Headers -APIName $APIName + try { + $Result = Clear-CIPPImmutableID -UserID $UserID -TenantFilter $TenantFilter -Headers $Headers -APIName $APIName $StatusCode = [HttpStatusCode]::OK } catch { - $ErrorMessage = Get-CippException -Exception $_ - $Result = $ErrorMessage.NormalizedError + $Result = $_.Exception.Message $StatusCode = [HttpStatusCode]::InternalServerError } - $Results = [pscustomobject]@{'Results' = $Result } # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ StatusCode = $StatusCode - Body = $Results + Body = @{'Results' = $Result } }) } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecCreateTAP.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecCreateTAP.ps1 index 58187b2f6640..d8b6022c735d 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecCreateTAP.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecCreateTAP.ps1 @@ -26,14 +26,14 @@ Function Invoke-ExecCreateTAP { $TAPResult, @{ resultText = "User ID: $UserID" - copyField = $UserID - state = 'success' + copyField = $UserID + state = 'success' } ) $StatusCode = [HttpStatusCode]::OK } catch { - $Results = Get-NormalizedError -message $($_.Exception.Message) + $Results = Get-NormalizedError -message $_.Exception.Message $StatusCode = [HttpStatusCode]::InternalServerError } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecDisableUser.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecDisableUser.ps1 index 67a2b036cffb..c1e6748b7f32 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecDisableUser.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecDisableUser.ps1 @@ -29,11 +29,10 @@ Function Invoke-ExecDisableUser { $StatusCode = [HttpStatusCode]::InternalServerError } - $Results = [pscustomobject]@{'Results' = "$Result" } # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ StatusCode = $StatusCode - Body = $Results + Body = @{ 'Results' = "$Result" } }) } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecDismissRiskyUser.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecDismissRiskyUser.ps1 index d22cca0f8e9e..a1e07c97a31a 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecDismissRiskyUser.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecDismissRiskyUser.ps1 @@ -29,8 +29,8 @@ function Invoke-ExecDismissRiskyUser { try { $GraphResults = New-GraphPostRequest @GraphRequest - Write-LogMessage -API $APIName -tenant $TenantFilter -message "Dismissed user risk for $userDisplayName" -sev 'Info' $Result = "Successfully dismissed User Risk for user $userDisplayName. $GraphResults" + Write-LogMessage -API $APIName -tenant $TenantFilter -message $Result -sev 'Info' $StatusCode = [HttpStatusCode]::OK } catch { $ErrorMessage = Get-CippException -Exception $_ diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecJITAdmin.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecJITAdmin.ps1 index e67fb1cb32d0..ae45ad066f45 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecJITAdmin.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecJITAdmin.ps1 @@ -125,14 +125,14 @@ function Invoke-ExecJITAdmin { if ($Request.Body.existingUser.value -match '^[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}$') { $Username = (New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/users/$($Request.Body.existingUser.value)" -tenantid $TenantFilter).userPrincipalName } - Write-LogMessage -Headers $User -API $APINAME -message "Executing JIT Admin for $Username" -tenant $TenantFilter -Sev 'Info' + Write-LogMessage -Headers $User -API $APIName -message "Executing JIT Admin for $Username" -tenant $TenantFilter -Sev 'Info' $Start = ([System.DateTimeOffset]::FromUnixTimeSeconds($Request.Body.StartDate)).DateTime.ToLocalTime() $Expiration = ([System.DateTimeOffset]::FromUnixTimeSeconds($Request.Body.EndDate)).DateTime.ToLocalTime() $Results = [System.Collections.Generic.List[string]]::new() if ($Request.Body.useraction -eq 'Create') { - Write-LogMessage -Headers $User -API $APINAME -tenant $TenantFilter -message "Creating JIT Admin user $($Request.Body.Username)" -Sev 'Info' + Write-LogMessage -Headers $User -API $APIName -tenant $TenantFilter -message "Creating JIT Admin user $($Request.Body.Username)" -Sev 'Info' Write-Information "Creating JIT Admin user $($Request.Body.username)" $JITAdmin = @{ User = @{ @@ -262,6 +262,8 @@ function Invoke-ExecJITAdmin { } } + # TODO - We should find a way to have this return a HTTP status code based on the success or failure of the operation. This also doesn't return the results of the operation in a Results hash table, like most of the rest of the API. + # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::OK Body = $Body diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecOneDriveShortCut.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecOneDriveShortCut.ps1 index 5da791680ea4..5bf461f6a0d5 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecOneDriveShortCut.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecOneDriveShortCut.ps1 @@ -15,16 +15,17 @@ Function Invoke-ExecOneDriveShortCut { Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' Try { - $MessageResult = New-CIPPOneDriveShortCut -username $Request.Body.username -userid $Request.Body.userid -TenantFilter $Request.Body.tenantFilter -URL $Request.Body.siteUrl.value -Headers $Request.Headers - $Results = [pscustomobject]@{ 'Results' = "$MessageResult" } + $Result = New-CIPPOneDriveShortCut -username $Request.Body.username -userid $Request.Body.userid -TenantFilter $Request.Body.tenantFilter -URL $Request.Body.siteUrl.value -Headers $Request.Headers + $StatusCode = [HttpStatusCode]::OK } catch { - $Results = [pscustomobject]@{'Results' = "OneDrive Shortcut creation failed: $($_.Exception.Message)" } + $Result = $_.Exception.Message + $StatusCode = [HttpStatusCode]::InternalServerError } # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ - StatusCode = [HttpStatusCode]::OK - Body = $Results + StatusCode = $StatusCode + Body = @{'Results' = $Result } }) } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecOnedriveProvision.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecOnedriveProvision.ps1 index d86806c39f68..48d6be9274fb 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecOnedriveProvision.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecOnedriveProvision.ps1 @@ -11,18 +11,23 @@ Function Invoke-ExecOneDriveProvision { param($Request, $TriggerMetadata) $APIName = $Request.Params.CIPPEndpoint - $Params = $Request.Body ?? $Request.Query + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + + $UserPrincipalName = $Request.Body.UserPrincipalName ?? $Request.Query.UserPrincipalName + $TenantFilter = $Request.Body.tenantFilter ?? $Request.Query.tenantFilter + try { - $State = Request-CIPPSPOPersonalSite -TenantFilter $Params.TenantFilter -UserEmails $Params.UserPrincipalName -Headers $Request.Headers -APIName $APINAME - $Results = [pscustomobject]@{'Results' = "$State" } + $Result = Request-CIPPSPOPersonalSite -TenantFilter $TenantFilter -UserEmails $UserPrincipalName -Headers $Headers -APIName $APIName + $StatusCode = [HttpStatusCode]::OK } catch { - $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - $Results = [pscustomobject]@{'Results' = "Failed. $ErrorMessage" } + $Result = $_.Exception.Message + $StatusCode = [HttpStatusCode]::InternalServerError } # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ - StatusCode = [HttpStatusCode]::OK - Body = $Results + StatusCode = $StatusCode + Body = @{'Results' = $Result } }) } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecResetMFA.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecResetMFA.ps1 index 4cb4c25db418..a72a7f82e40e 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecResetMFA.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecResetMFA.ps1 @@ -12,17 +12,17 @@ Function Invoke-ExecResetMFA { $APIName = $Request.Params.CIPPEndpoint $Headers = $Request.Headers - Write-LogMessage -headers $Headers -API $APINAME -message 'Accessed this API' -Sev 'Debug' + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' # Interact with query parameters or the body of the request. $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter $UserID = $Request.Query.ID ?? $Request.Body.ID try { $Result = Remove-CIPPUserMFA -UserPrincipalName $UserID -TenantFilter $TenantFilter -Headers $Headers - if ($Result -match 'Failed') { throw $Result } + if ($Result -match '^Failed') { throw $Result } $StatusCode = [HttpStatusCode]::OK } catch { - $Result = "$($_.Exception.Message)" + $Result = $_.Exception.Message $StatusCode = [HttpStatusCode]::InternalServerError } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecResetPass.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecResetPass.ps1 index 11fed934022b..d2080b726ab8 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecResetPass.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecResetPass.ps1 @@ -23,21 +23,18 @@ Function Invoke-ExecResetPass { $MustChange = [System.Convert]::ToBoolean($MustChange) try { - $Result = Set-CIPPResetPassword -UserID $ID -tenantFilter $TenantFilter -APIName $APINAME -Headers $Request.Headers -forceChangePasswordNextSignIn $MustChange -DisplayName $DisplayName + $Result = Set-CIPPResetPassword -UserID $ID -tenantFilter $TenantFilter -APIName $APIName -Headers $Headers -forceChangePasswordNextSignIn $MustChange -DisplayName $DisplayName if ($Result.state -eq 'Error') { throw $Result.resultText } $StatusCode = [HttpStatusCode]::OK } catch { $Result = $_.Exception.Message - Write-LogMessage -headers $Request.Headers -API $APINAME -message $Result -Sev 'Error' $StatusCode = [HttpStatusCode]::InternalServerError - } - $Results = [pscustomobject]@{'Results' = $Result } # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ StatusCode = $StatusCode - Body = $Results + Body = @{'Results' = $Result } }) } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecRestoreDeleted.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecRestoreDeleted.ps1 index cf4300a8054d..36daf3d8914c 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecRestoreDeleted.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecRestoreDeleted.ps1 @@ -39,11 +39,10 @@ Function Invoke-ExecRestoreDeleted { $StatusCode = [HttpStatusCode]::InternalServerError } - $Results = [pscustomobject]@{'Results' = $Result } # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ StatusCode = $StatusCode - Body = $Results + Body = @{'Results' = $Result } }) } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecRevokeSessions.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecRevokeSessions.ps1 index 822c6356dcf1..4d39d67fbbec 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecRevokeSessions.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecRevokeSessions.ps1 @@ -21,18 +21,17 @@ Function Invoke-ExecRevokeSessions { try { $Result = Revoke-CIPPSessions -UserID $ID -TenantFilter $TenantFilter -Username $Username -APIName $APIName -Headers $Request.Headers - if ($Result -like 'Revoke Session Failed*') { throw $Result } + if ($Result -match '^Failed') { throw $Result } $StatusCode = [HttpStatusCode]::OK } catch { $Result = $_.Exception.Message $StatusCode = [HttpStatusCode]::InternalServerError } - $Results = [pscustomobject]@{'Results' = $Result } # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ StatusCode = $StatusCode - Body = $Results + Body = @{'Results' = $Result } }) } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListPerUserMFA.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListPerUserMFA.ps1 index 7e6d040a18d9..2c6db5ca62ef 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListPerUserMFA.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListPerUserMFA.ps1 @@ -12,9 +12,7 @@ function Invoke-ListPerUserMFA { $APIName = $Request.Params.CIPPEndpoint $User = $Request.Headers - Write-LogMessage -Headers $User -API $APINAME -message 'Accessed this API' -Sev 'Debug' - - + Write-LogMessage -Headers $User -API $APIName -message 'Accessed this API' -Sev 'Debug' # Parse query parameters $Tenant = $Request.query.tenantFilter @@ -30,13 +28,13 @@ function Invoke-ListPerUserMFA { if ($AllUsers -eq $true) { $Results = Get-CIPPPerUserMFA -TenantFilter $Tenant -AllUsers $true } else { - $Results = Get-CIPPPerUserMFA -TenantFilter $Tenant -userId $UserId + $Results = Get-CIPPPerUserMFA -TenantFilter $Tenant -UserId $UserId } $StatusCode = [HttpStatusCode]::OK } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message $Results = "Failed to get MFA State for $UserId : $ErrorMessage" - $StatusCode = [HttpStatusCode]::Forbidden + $StatusCode = [HttpStatusCode]::InternalServerError } # Associate values to output bindings by calling 'Push-OutputBinding'. diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserConditionalAccessPolicies.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserConditionalAccessPolicies.ps1 index bd8ce3a7eb27..585fb8c71bbc 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserConditionalAccessPolicies.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserConditionalAccessPolicies.ps1 @@ -14,11 +14,10 @@ Function Invoke-ListUserConditionalAccessPolicies { $Headers = $Request.Headers Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' - - + # XXX - Unused endpoint? # Interact with query parameters or the body of the request. - $TenantFilter = $Request.Query.TenantFilter + $TenantFilter = $Request.Query.tenantFilter $UserID = $Request.Query.UserID try { @@ -30,14 +29,14 @@ Function Invoke-ListUserConditionalAccessPolicies { $ConditionalAccessWhatIfDefinition = @{ 'conditionalAccessWhatIfSubject' = @{ '@odata.type' = '#microsoft.graph.userSubject' - 'userId' = "$userId" + 'userId' = "$UserID" } 'conditionalAccessContext' = $CAContext 'conditionalAccessWhatIfConditions' = @{} } - $JSONBody = $ConditionalAccessWhatIfDefinition | ConvertTo-Json -Depth 10 + $JSONBody = ConvertTo-Json -Depth 10 -InputObject $ConditionalAccessWhatIfDefinition -Compress - $GraphRequest = (New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/evaluate' -tenantid $tenantFilter -type POST -body $JsonBody -AsApp $true).value + $GraphRequest = (New-GraphPostRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/evaluate' -tenantid $TenantFilter -type POST -body $JsonBody -AsApp $true).value } catch { $GraphRequest = @{} } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserCounts.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserCounts.ps1 index f9522b7648a9..e7d995268ad2 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserCounts.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserCounts.ps1 @@ -14,28 +14,25 @@ Function Invoke-ListUserCounts { $Headers = $Request.Headers Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' - - - # Interact with query parameters or the body of the request. $TenantFilter = $Request.Query.TenantFilter if ($Request.Query.TenantFilter -eq 'AllTenants') { - $users = 'Not Supported' + $Users = 'Not Supported' $LicUsers = 'Not Supported' $GAs = 'Not Supported' $Guests = 'Not Supported' } else { try { $Users = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users?`$count=true&`$top=1" -CountOnly -ComplexFilter -tenantid $TenantFilter } catch { $Users = 'Not available' } - try { $LicUsers = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users?`$count=true&`$top=1&`$filter=assignedLicenses/`$count ne 0" -CountOnly -ComplexFilter -tenantid $TenantFilter } catch { $Licusers = 'Not available' } - try { $GAs = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/directoryRoles/roleTemplateId=62e90394-69f5-4237-9190-012177145e10/members?`$count=true" -CountOnly -ComplexFilter -tenantid $TenantFilter } catch { $Gas = 'Not available' } - try { $guests = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users?`$count=true&`$top=1&`$filter=userType eq 'Guest'" -CountOnly -ComplexFilter -tenantid $TenantFilter } catch { $Guests = 'Not available' } + try { $LicUsers = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users?`$count=true&`$top=1&`$filter=assignedLicenses/`$count ne 0" -CountOnly -ComplexFilter -tenantid $TenantFilter } catch { $LicUsers = 'Not available' } + try { $GAs = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/directoryRoles/roleTemplateId=62e90394-69f5-4237-9190-012177145e10/members?`$count=true" -CountOnly -ComplexFilter -tenantid $TenantFilter } catch { $GAs = 'Not available' } + try { $Guests = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users?`$count=true&`$top=1&`$filter=userType eq 'Guest'" -CountOnly -ComplexFilter -tenantid $TenantFilter } catch { $Guests = 'Not available' } } $StatusCode = [HttpStatusCode]::OK $Counts = @{ - Users = $users + Users = $Users LicUsers = $LicUsers - Gas = $Gas - Guests = $guests + Gas = $GAs + Guests = $Guests } # Associate values to output bindings by calling 'Push-OutputBinding'. diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserDevices.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserDevices.ps1 index d37bc6e836b2..427b61b05f60 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserDevices.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserDevices.ps1 @@ -14,11 +14,8 @@ Function Invoke-ListUserDevices { $Headers = $Request.Headers Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' - - - # Interact with query parameters or the body of the request. - $TenantFilter = $Request.Query.TenantFilter + $TenantFilter = $Request.Query.tenantFilter $UserID = $Request.Query.UserID function Get-EPMID { @@ -33,8 +30,8 @@ Function Invoke-ListUserDevices { } } try { - $EPMDevices = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$UserID/managedDevices" -Tenantid $tenantfilter - $GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$UserID/ownedDevices?`$top=999" -Tenantid $tenantfilter | Select-Object @{ Name = 'ID'; Expression = { $_.'id' } }, + $EPMDevices = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$UserID/managedDevices" -Tenantid $TenantFilter + $GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$UserID/ownedDevices?`$top=999" -Tenantid $TenantFilter | Select-Object @{ Name = 'ID'; Expression = { $_.'id' } }, @{ Name = 'accountEnabled'; Expression = { $_.'accountEnabled' } }, @{ Name = 'approximateLastSignInDateTime'; Expression = { $_.'approximateLastSignInDateTime' | Out-String } }, @{ Name = 'createdDateTime'; Expression = { $_.'createdDateTime' | Out-String } }, 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 e1181a6893fc..86bb0bc44c5b 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 @@ -115,7 +115,7 @@ function Invoke-ListUserMailboxDetails { # Parse permissions - #Implemented as an arraylist that uses .add(). + #Implemented as an ArrayList that uses .add(). $ParsedPerms = [System.Collections.ArrayList]::new() foreach ($PermSet in @($PermsRequest, $PermsRequest2)) { foreach ($Perm in $PermSet) { @@ -177,7 +177,7 @@ function Invoke-ListUserMailboxDetails { $TotalArchiveItemCount = try { [math]::Round($ArchiveSizeRequest.ItemCount, 2) } catch { 0 } } - # Parse InPlaceHolds to determine hold types if avaliable + # Parse InPlaceHolds to determine hold types if available $InPlaceHold = $false $EDiscoveryHold = $false $PurviewRetentionHold = $false diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserMailboxRules.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserMailboxRules.ps1 index 74def3815f6d..750062e50315 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserMailboxRules.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserMailboxRules.ps1 @@ -11,33 +11,28 @@ Function Invoke-ListUserMailboxRules { param($Request, $TriggerMetadata) $APIName = $Request.Params.CIPPEndpoint - $User = $Request.Headers - Write-LogMessage -Headers $User -API $APINAME -message 'Accessed this API' -Sev 'Debug' - - - + $Headers = $Request.Headers + Write-LogMessage -Headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' # Interact with query parameters or the body of the request. + $TenantFilter = $Request.Query.tenantFilter + $UserID = $Request.Query.UserID try { - $TenantFilter = $Request.Query.TenantFilter - $UserID = $Request.Query.UserID $UserEmail = if ([string]::IsNullOrWhiteSpace($Request.Query.userEmail)) { $UserID } else { $Request.Query.userEmail } - $GraphRequest = New-ExoRequest -Anchor $UserID -tenantid $TenantFilter -cmdlet 'Get-InboxRule' -cmdParams @{mailbox = $UserID; IncludeHidden = $true } | Where-Object { $_.Name -ne 'Junk E-Mail Rule' -and $_.Name -notlike 'Microsoft.Exchange.OOF.*' } | Select-Object * -ExcludeProperty RuleIdentity + $Result = New-ExoRequest -Anchor $UserID -tenantid $TenantFilter -cmdlet 'Get-InboxRule' -cmdParams @{mailbox = $UserID; IncludeHidden = $true } | + Where-Object { $_.Name -ne 'Junk E-Mail Rule' -and $_.Name -notlike 'Microsoft.Exchange.OOF.*' } | Select-Object * -ExcludeProperty RuleIdentity + $StatusCode = [HttpStatusCode]::OK } catch { $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -Headers $User -API $APINAME -message "Failed to retrieve mailbox rules $($UserEmail): $($ErrorMessage.NormalizedError) " -Sev 'Error' -tenant $TenantFilter -LogData $ErrorMessage - # Associate values to output bindings by calling 'Push-OutputBinding'. - Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ - StatusCode = '500' - Body = $ErrorMessage.NormalizedError - }) - exit + $Result = "Failed to retrieve mailbox rules for $UserEmail : Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -Headers $Headers -tenant $TenantFilter -API $APIName -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 = [HttpStatusCode]::OK - Body = @($GraphRequest) + StatusCode = $StatusCode + Body = @($Result) }) } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserPhoto.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserPhoto.ps1 index 154dc1fffaa6..19ceb7562341 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserPhoto.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserPhoto.ps1 @@ -10,7 +10,12 @@ Function Invoke-ListUserPhoto { [CmdletBinding()] param($Request, $TriggerMetadata) - $tenantFilter = $Request.Query.TenantFilter + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -Headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + + # Interact with query parameters or the body of the request. + $tenantFilter = $Request.Query.tenantFilter $userId = $Request.Query.UserID $URI = "/users/$userId/photo/`$value" diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserSettings.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserSettings.ps1 index 858ddfb27da4..224584ab81af 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserSettings.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserSettings.ps1 @@ -12,12 +12,13 @@ function Invoke-ListUserSettings { $APIName = $Request.Params.CIPPEndpoint $Headers = $Request.Headers Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' - $username = ([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($request.headers.'x-ms-client-principal')) | ConvertFrom-Json).userDetails + + $Username = ([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Headers.'x-ms-client-principal')) | ConvertFrom-Json).userDetails try { $Table = Get-CippTable -tablename 'UserSettings' $UserSettings = Get-CIPPAzDataTableEntity @Table -Filter "RowKey eq 'allUsers'" - if (!$UserSettings) { $userSettings = Get-CIPPAzDataTableEntity @Table -Filter "RowKey eq '$username'" } + if (!$UserSettings) { $UserSettings = Get-CIPPAzDataTableEntity @Table -Filter "RowKey eq '$Username'" } $UserSettings = $UserSettings.JSON | ConvertFrom-Json -Depth 10 -ErrorAction SilentlyContinue $StatusCode = [HttpStatusCode]::OK $Results = $UserSettings diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserSigninLogs.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserSigninLogs.ps1 index a0009394dd72..1804d4b4384a 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserSigninLogs.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserSigninLogs.ps1 @@ -18,26 +18,22 @@ Function Invoke-ListUserSigninLogs { # Interact with query parameters or the body of the request. - $TenantFilter = $Request.Query.TenantFilter + $TenantFilter = $Request.Query.tenantFilter $UserID = $Request.Query.UserID + $URI = "https://graph.microsoft.com/beta/auditLogs/signIns?`$filter=(userId eq '$UserID')&`$top=$top&`$orderby=createdDateTime desc" + try { - $URI = "https://graph.microsoft.com/beta/auditLogs/signIns?`$filter=(userId eq '$UserID')&`$top=$top&`$orderby=createdDateTime desc" - Write-Host $URI - $GraphRequest = New-GraphGetRequest -uri $URI -tenantid $TenantFilter -noPagination $true -verbose - Write-Host $GraphRequest - # Associate values to output bindings by calling 'Push-OutputBinding'. - Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ - StatusCode = [HttpStatusCode]::OK - Body = @($GraphRequest) - }) + $Result = New-GraphGetRequest -uri $URI -tenantid $TenantFilter -noPagination $true -verbose + $StatusCode = [HttpStatusCode]::OK } catch { - Write-LogMessage -headers $Request.Headers -API $APINAME -message "Failed to retrieve Sign In report: $($_.Exception.message) " -Sev 'Error' -tenant $TenantFilter - # Associate values to output bindings by calling 'Push-OutputBinding'. - Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ - StatusCode = '500' - Body = $(Get-NormalizedError -message $_.Exception.message) - }) + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to retrieve Sign In report for user $UserID : Error: $($ErrorMessage.NormalizedError)" + $StatusCode = [HttpStatusCode]::InternalServerError } - + # 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/Identity/Administration/Users/Invoke-ListUsers.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUsers.ps1 index d2a5c2685bd3..5e81ea1c684b 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUsers.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUsers.ps1 @@ -16,7 +16,7 @@ Function Invoke-ListUsers { $ConvertTable = Import-Csv ConversionTable.csv | Sort-Object -Property 'guid' -Unique # Interact with query parameters or the body of the request. - $TenantFilter = $Request.Query.TenantFilter + $TenantFilter = $Request.Query.tenantFilter $GraphFilter = $Request.Query.graphFilter $userid = $Request.Query.UserID @@ -26,7 +26,7 @@ Function Invoke-ListUsers { $_ | Add-Member -MemberType NoteProperty -Name 'username' -Value ($_.userPrincipalName -split '@' | Select-Object -First 1) -Force $_ | Add-Member -MemberType NoteProperty -Name 'Aliases' -Value ($_.ProxyAddresses -join ', ') -Force $SkuID = $_.AssignedLicenses.skuid - $_ | Add-Member -MemberType NoteProperty -Name 'LicJoined' -Value (($ConvertTable | Where-Object { $_.guid -in $skuid }).'Product_Display_Name' -join ', ') -Force + $_ | Add-Member -MemberType NoteProperty -Name 'LicJoined' -Value (($ConvertTable | Where-Object { $_.guid -in $SkuID }).'Product_Display_Name' -join ', ') -Force $_ | Add-Member -MemberType NoteProperty -Name 'primDomain' -Value @{value = ($_.userPrincipalName -split '@' | Select-Object -Last 1); label = ($_.userPrincipalName -split '@' | Select-Object -Last 1); } -Force $_ } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-RemoveDeletedObject.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-RemoveDeletedObject.ps1 index da4e516d6462..f41f79d68485 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-RemoveDeletedObject.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-RemoveDeletedObject.ps1 @@ -39,11 +39,10 @@ Function Invoke-RemoveDeletedObject { $StatusCode = [HttpStatusCode]::InternalServerError } - $Results = [pscustomobject]@{'Results' = $Result } # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ StatusCode = $StatusCode - Body = $Results + Body = @{'Results' = $Result } }) } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-AddSite.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-AddSite.ps1 index e8f19acf0534..13eb06e766b1 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-AddSite.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-AddSite.ps1 @@ -14,21 +14,23 @@ Function Invoke-AddSite { $Headers = $Request.Headers Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' - $SharePointObj = $Request.body + + # Interact with query parameters or the body of the request. + $TenantFilter = $Request.Body.tenantFilter + $SharePointObj = $Request.Body try { - $SharePointSite = New-CIPPSharepointSite -SiteName $SharePointObj.siteName -SiteDescription $SharePointObj.siteDescription -SiteOwner $SharePointObj.siteOwner.value -TemplateName $SharePointObj.templateName.value -SiteDesign $SharePointObj.siteDesign.value -SensitivityLabel $SharePointObj.sensitivityLabel -TenantFilter $SharePointObj.tenantFilter - $body = [pscustomobject]@{'Results' = $SharePointSite } + $Result = New-CIPPSharepointSite -Headers $Headers -SiteName $SharePointObj.siteName -SiteDescription $SharePointObj.siteDescription -SiteOwner $SharePointObj.siteOwner.value -TemplateName $SharePointObj.templateName.value -SiteDesign $SharePointObj.siteDesign.value -SensitivityLabel $SharePointObj.sensitivityLabel -TenantFilter $TenantFilter + $StatusCode = [HttpStatusCode]::OK } catch { - Write-LogMessage -headers $Request.Headers -API $APINAME -tenant $($userobj.tenantid) -message "Adding SharePoint Site failed. Error: $($_.Exception.Message)" -Sev 'Error' - $body = [pscustomobject]@{'Results' = "Failed. Error message: $($_.Exception.Message)" } + $StatusCode = [HttpStatusCode]::InternalServerError + $Result = "Failed to create SharePoint Site: $($_.Exception.Message)" } - # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ - StatusCode = [HttpStatusCode]::OK - Body = $Body + StatusCode = $StatusCode + Body = @{'Results' = $Result } }) } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-AddSiteBulk.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-AddSiteBulk.ps1 index 8ca292b2c52f..bbb63aebc399 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-AddSiteBulk.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-AddSiteBulk.ps1 @@ -15,22 +15,20 @@ Function Invoke-AddSiteBulk { Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' - $Results = [System.Collections.ArrayList]@() + $Results = [System.Collections.Generic.List[System.Object]]::new() - foreach ($sharepointObj in $Request.Body.bulkSites) { + foreach ($sharePointObj in $Request.Body.bulkSites) { try { - $SharePointSite = New-CIPPSharepointSite -SiteName $SharePointObj.siteName -SiteDescription $SharePointObj.siteDescription -SiteOwner $SharePointObj.siteOwner -TemplateName $SharePointObj.templateName -SiteDesign $SharePointObj.siteDesign -SensitivityLabel $SharePointObj.sensitivityLabel -TenantFilter $Request.body.TenantFilter - $Results.add($SharePointSite) + $SharePointSite = New-CIPPSharepointSite -Headers $Headers -SiteName $sharePointObj.siteName -SiteDescription $sharePointObj.siteDescription -SiteOwner $sharePointObj.siteOwner -TemplateName $sharePointObj.templateName -SiteDesign $sharePointObj.siteDesign -SensitivityLabel $sharePointObj.sensitivityLabel -TenantFilter $Request.body.tenantFilter + $Results.Add($SharePointSite) } catch { - Write-LogMessage -headers $Request.Headers -API $APINAME -tenant $($userobj.tenantid) -message "Adding SharePoint Site failed. Error: $($_.Exception.Message)" -Sev 'Error' - $Results.add("Failed to create $($sharepointObj.siteName) Error message: $($_.Exception.Message)") + $Results.Add("Failed to create $($sharePointObj.siteName) Error message: $($_.Exception.Message)") } } - $Body = [pscustomobject]@{'Results' = $Results } # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::OK - Body = $Body + Body = @{'Results' = $Results } }) } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ExecRemoveTeamsVoicePhoneNumberAssignment.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ExecRemoveTeamsVoicePhoneNumberAssignment.ps1 index b022aee68d46..2e1835cba80e 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ExecRemoveTeamsVoicePhoneNumberAssignment.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ExecRemoveTeamsVoicePhoneNumberAssignment.ps1 @@ -14,19 +14,26 @@ Function Invoke-ExecRemoveTeamsVoicePhoneNumberAssignment { $Headers = $Request.Headers Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' - $tenantFilter = $Request.Body.TenantFilter + # Interact with query parameters or the body of the request. + $TenantFilter = $Request.Body.tenantFilter + $AssignedTo = $Request.Body.AssignedTo + $PhoneNumber = $Request.Body.PhoneNumber + $PhoneNumberType = $Request.Body.PhoneNumberType + try { - $null = New-TeamsRequest -TenantFilter $TenantFilter -Cmdlet 'Remove-CsPhoneNumberAssignment' -CmdParams @{Identity = $Request.Body.AssignedTo; PhoneNumber = $Request.Body.PhoneNumber; PhoneNumberType = $Request.Body.PhoneNumberType; ErrorAction = 'stop'} - $Results = [pscustomobject]@{'Results' = "Successfully unassigned $($Request.Body.PhoneNumber) from $($Request.Body.AssignedTo)"} - Write-LogMessage -headers $Request.Headers -API $APINAME -tenant $($TenantFilter) -message $($Results.Results) -Sev 'Info' + $null = New-TeamsRequest -TenantFilter $TenantFilter -Cmdlet 'Remove-CsPhoneNumberAssignment' -CmdParams @{Identity = $AssignedTo; PhoneNumber = $PhoneNumber; PhoneNumberType = $PhoneNumberType; ErrorAction = 'Stop' } + $Result = "Successfully unassigned $PhoneNumber from $AssignedTo" + Write-LogMessage -headers $Headers -API $APIName -tenant $($TenantFilter) -message $Result -Sev 'Info' + $StatusCode = [HttpStatusCode]::OK } catch { - $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - $Results = [pscustomobject]@{'Results' = $ErrorMessage} - Write-LogMessage -headers $Request.Headers -API $APINAME -tenant $($TenantFilter) -message $($Results.Results) -Sev 'Error' + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to unassign $PhoneNumber from $AssignedTo. Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -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 = [HttpStatusCode]::OK - Body = $Results + StatusCode = $StatusCode + Body = @{'Results' = $Result } }) } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ExecSetSharePointMember.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ExecSetSharePointMember.ps1 index 0894c81fbdf1..3d8e9947ff77 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ExecSetSharePointMember.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ExecSetSharePointMember.ps1 @@ -13,28 +13,34 @@ Function Invoke-ExecSetSharePointMember { $APIName = $Request.Params.CIPPEndpoint $Headers = $Request.Headers - Write-LogMessage -Headers $Headers -API $APINAME -message 'Accessed this API' -Sev 'Debug' - $TenantFilter = $Request.body.tenantFilter - - - - if ($Request.body.SharePointType -eq 'Group') { - $GroupId = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/groups?`$filter=mail eq '$($Request.Body.GroupID)' or proxyAddresses/any(x:endsWith(x,'$($Request.Body.GroupID)'))&`$count=true" -ComplexFilter -tenantid $TenantFilter).id - if ($Request.body.Add -eq $true) { - $Results = Add-CIPPGroupMember -GroupType 'Team' -GroupID $GroupID -Member $Request.Body.user.value -TenantFilter $TenantFilter -Headers $Request.Headers + Write-LogMessage -Headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + + # Interact with query parameters or the body of the request. + $TenantFilter = $Request.Body.tenantFilter + + try { + if ($Request.Body.SharePointType -eq 'Group') { + $GroupId = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/groups?`$filter=mail eq '$($Request.Body.GroupID)' or proxyAddresses/any(x:endsWith(x,'$($Request.Body.GroupID)'))&`$count=true" -ComplexFilter -tenantid $TenantFilter).id + if ($Request.Body.Add -eq $true) { + $Results = Add-CIPPGroupMember -GroupType 'Team' -GroupID $GroupID -Member $Request.Body.user.value -TenantFilter $TenantFilter -Headers $Headers + } else { + $UserID = (New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/users/$($Request.Body.user.value)" -tenantid $TenantFilter).id + $Results = Remove-CIPPGroupMember -GroupType 'Team' -GroupID $GroupID -Member $UserID -TenantFilter $TenantFilter -Headers $Headers + } } else { - $UserID = (New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/users/$($Request.Body.user.value)" -tenantid $TenantFilter).id - $Results = Remove-CIPPGroupMember -GroupType 'Team' -GroupID $GroupID -Member $UserID -TenantFilter $TenantFilter -Headers $Request.Headers + $StatusCode = [HttpStatusCode]::BadRequest + $Results = 'This type of SharePoint site is not supported.' } - } else { - $Results = 'This type of SharePoint site is not supported.' + } catch { + $Results = $_.Exception.Message + $StatusCode = [HttpStatusCode]::InternalServerError } - $body = [pscustomobject]@{'Results' = $Results } + # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ - StatusCode = [HttpStatusCode]::OK - Body = $body + StatusCode = $StatusCode + Body = @{ 'Results' = $Results } }) } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ExecSharePointPerms.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ExecSharePointPerms.ps1 index e2c91f14a12e..2952dc7c645b 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ExecSharePointPerms.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ExecSharePointPerms.ps1 @@ -11,11 +11,11 @@ Function Invoke-ExecSharePointPerms { param($Request, $TriggerMetadata) $APIName = $Request.Params.CIPPEndpoint - $tenantFilter = $Request.Body.tenantFilter $Headers = $Request.Headers - Write-LogMessage -Headers $Headers -API $APIName -message 'Accessed this API' -Sev Debug + $TenantFilter = $Request.Body.tenantFilter + Write-Host '====================================' Write-Host 'Request Body:' Write-Host (ConvertTo-Json $Request.body -Depth 10) @@ -31,7 +31,7 @@ Function Invoke-ExecSharePointPerms { try { - $State = Set-CIPPSharePointPerms -tenantFilter $tenantFilter ` + $State = Set-CIPPSharePointPerms -tenantFilter $TenantFilter ` -UserId $UserId ` -OnedriveAccessUser $OnedriveAccessUser ` -Headers $Headers ` @@ -41,8 +41,8 @@ Function Invoke-ExecSharePointPerms { $Result = "$State" $StatusCode = [HttpStatusCode]::OK } catch { - $ErrorMessage = Get-CippException -Exception $_ - $Result = "Failed. $($ErrorMessage.NormalizedError)" + $ErrorMessage = $_.Exception.Message + $Result = "Failed. Error: $ErrorMessage" $StatusCode = [HttpStatusCode]::BadRequest } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointQuota.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointQuota.ps1 index d09419fcc476..d32656423abb 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointQuota.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointQuota.ps1 @@ -15,30 +15,30 @@ Function Invoke-ListSharepointQuota { Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' # Interact with query parameters or the body of the request. - $TenantFilter = $Request.Query.TenantFilter + $TenantFilter = $Request.Query.tenantFilter - if ($Request.Query.TenantFilter -eq 'AllTenants') { + if ($TenantFilter -eq 'AllTenants') { $UsedStoragePercentage = 'Not Supported' } else { try { - $tenantName = (New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/sites/root' -asApp $true -tenantid $TenantFilter).id.Split('.')[0] + $TenantName = (New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/sites/root' -asApp $true -tenantid $TenantFilter).id.Split('.')[0] - $sharepointToken = (Get-GraphToken -scope "https://$($tenantName)-admin.sharepoint.com/.default" -tenantid $TenantFilter) - $sharepointToken.Add('accept', 'application/json') - # Implement a try catch later to deal with sharepoint guest user settings - $sharepointQuota = (Invoke-RestMethod -Method 'GET' -Headers $sharepointToken -Uri "https://$($tenantName)-admin.sharepoint.com/_api/StorageQuotas()?api-version=1.3.2" -ErrorAction Stop).value | Sort-Object -Property GeoUsedStorageMB -Descending | Select-Object -First 1 + $SharePointToken = (Get-GraphToken -scope "https://$($TenantName)-admin.sharepoint.com/.default" -tenantid $TenantFilter) + $SharePointToken.Add('accept', 'application/json') + # Implement a try catch later to deal with SharePoint guest user settings + $SharePointQuota = (Invoke-RestMethod -Method 'GET' -Headers $SharePointToken -Uri "https://$($TenantName)-admin.sharepoint.com/_api/StorageQuotas()?api-version=1.3.2" -ErrorAction Stop).value | Sort-Object -Property GeoUsedStorageMB -Descending | Select-Object -First 1 - if ($sharepointQuota) { - $UsedStoragePercentage = [int](($sharepointQuota.GeoUsedStorageMB / $sharepointQuota.TenantStorageMB) * 100) + if ($SharePointQuota) { + $UsedStoragePercentage = [int](($SharePointQuota.GeoUsedStorageMB / $SharePointQuota.TenantStorageMB) * 100) } } catch { $UsedStoragePercentage = 'Not available' } } - $sharepointQuotaDetails = @{ - GeoUsedStorageMB = $sharepointQuota.GeoUsedStorageMB - TenantStorageMB = $sharepointQuota.TenantStorageMB + $SharePointQuotaDetails = @{ + GeoUsedStorageMB = $SharePointQuota.GeoUsedStorageMB + TenantStorageMB = $SharePointQuota.TenantStorageMB Percentage = $UsedStoragePercentage Dashboard = "$($UsedStoragePercentage) / 100" } @@ -48,7 +48,7 @@ Function Invoke-ListSharepointQuota { # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ StatusCode = $StatusCode - Body = $sharepointQuotaDetails + Body = $SharePointQuotaDetails }) } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointSettings.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointSettings.ps1 index cb84b80a8b6e..fdf4db9675f2 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointSettings.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointSettings.ps1 @@ -14,21 +14,17 @@ Function Invoke-ListSharepointSettings { $Headers = $Request.Headers Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' - + # XXX - Seems to be an unused endpoint? -Bobby # Interact with query parameters or the body of the request. - $tenant = $Request.Query.TenantFilter - $User = $Request.query.user - $USERToGet = $Request.query.usertoGet - $body = '{"isResharingByExternalUsersEnabled": "False"}' - $Request = New-GraphPostRequest -tenantid $tenant -Uri 'https://graph.microsoft.com/beta/admin/sharepoint/settings' -Type patch -Body $body -ContentType 'application/json' + $Tenant = $Request.Query.tenantFilter + $Request = New-GraphGetRequest -tenantid $Tenant -Uri 'https://graph.microsoft.com/beta/admin/sharepoint/settings' - Write-LogMessage -API 'Standards' -tenant $tenantFilter -message 'Disabled Password Expiration' -sev Info # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::OK - Body = @($GraphRequest) + Body = @($Request) }) } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSites.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSites.ps1 index 2d23640c9fcd..84b64bbeb7ae 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSites.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSites.ps1 @@ -10,6 +10,10 @@ Function Invoke-ListSites { [CmdletBinding()] param($Request, $TriggerMetadata) + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -Headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + $TenantFilter = $Request.Query.TenantFilter $Type = $request.query.Type $UserUPN = $request.query.UserUPN @@ -93,7 +97,7 @@ Function Invoke-ListSites { try { $Requests = (New-GraphBulkRequest -tenantid $TenantFilter -scope 'https://graph.microsoft.com/.default' -Requests @($Requests) -asapp $true).body.value | Where-Object { $_.list.template -eq 'DocumentLibrary' } } catch { - Write-LogMessage -Message "Error getting auto map urls: $($_.Exception.Message)" -Sev 'Error' -tenant $TenantFilter -API 'ListSites' -LogData (Get-CippException -Exception $_) + Write-LogMessage -Headers $Headers -Message "Error getting auto map urls: $($_.Exception.Message)" -Sev 'Error' -tenant $TenantFilter -API 'ListSites' -LogData (Get-CippException -Exception $_) } $GraphRequest = foreach ($Site in $GraphRequest) { $ListId = ($Requests | Where-Object { $_.parentReference.siteId -like "*$($Site.siteId)*" }).id diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListTeams.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListTeams.ps1 index 38637679c79c..4d6d55619c15 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListTeams.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListTeams.ps1 @@ -14,13 +14,10 @@ Function Invoke-ListTeams { $Headers = $Request.Headers Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' - - - # Interact with query parameters or the body of the request. $TenantFilter = $Request.Query.TenantFilter if ($request.query.type -eq 'List') { - $GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/groups?`$filter=resourceProvisioningOptions/Any(x:x eq 'Team')&`$select=id,displayname,description,visibility,mailNickname" -tenantid $TenantFilter | Sort-Object -Property displayName + $GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/groups?`$filter=resourceProvisioningOptions/Any(x:x eq 'Team')&`$select=id,displayName,description,visibility,mailNickname" -tenantid $TenantFilter | Sort-Object -Property displayName } $TeamID = $request.query.ID Write-Host $TeamID diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListTeamsActivity.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListTeamsActivity.ps1 index 342f8096d272..9323a71f30a1 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListTeamsActivity.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListTeamsActivity.ps1 @@ -10,12 +10,14 @@ Function Invoke-ListTeamsActivity { [CmdletBinding()] param($Request, $TriggerMetadata) - + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' # Interact with query parameters or the body of the request. - $TenantFilter = $Request.Query.TenantFilter - $type = $request.query.Type + $TenantFilter = $Request.Query.tenantFilter + $type = $request.Query.Type $GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/reports/get$($type)Detail(period='D30')" -tenantid $TenantFilter | ConvertFrom-Csv | Select-Object @{ Name = 'UPN'; Expression = { $_.'User Principal Name' } }, @{ Name = 'LastActive'; Expression = { $_.'Last Activity Date' } }, @{ Name = 'TeamsChat'; Expression = { $_.'Team Chat Message Count' } }, diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-AddAlert.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-AddAlert.ps1 index 8fcf39248cd2..72872dce00a6 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-AddAlert.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-AddAlert.ps1 @@ -12,30 +12,31 @@ Function Invoke-AddAlert { $APIName = $Request.Params.CIPPEndpoint $Headers = $Request.Headers Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' - $Tenants = $request.body.tenantFilter - $Conditions = $request.body.conditions | ConvertTo-Json -Compress -Depth 10 | Out-String + + # Interact with query parameters or the body of the request. + $Tenants = $Request.Body.tenantFilter + $Conditions = $Request.Body.conditions | ConvertTo-Json -Compress -Depth 10 | Out-String $TenantsJson = $Tenants | ConvertTo-Json -Compress -Depth 10 | Out-String - $excludedTenantsJson = $request.body.excludedTenants | ConvertTo-Json -Compress -Depth 10 | Out-String - $Actions = $request.body.actions | ConvertTo-Json -Compress -Depth 10 | Out-String - $RowKey = $Request.body.RowKey ? $Request.body.RowKey : (New-Guid).ToString() + $excludedTenantsJson = $Request.Body.excludedTenants | ConvertTo-Json -Compress -Depth 10 | Out-String + $Actions = $Request.Body.actions | ConvertTo-Json -Compress -Depth 10 | Out-String + $RowKey = $Request.Body.RowKey ? $Request.Body.RowKey : (New-Guid).ToString() $CompleteObject = @{ Tenants = [string]$TenantsJson excludedTenants = [string]$excludedTenantsJson Conditions = [string]$Conditions Actions = [string]$Actions - type = $request.body.logbook.value + type = $Request.Body.logbook.value RowKey = $RowKey PartitionKey = 'Webhookv2' } - $WebhookTable = get-cipptable -TableName 'WebhookRules' + $WebhookTable = Get-CippTable -TableName 'WebhookRules' Add-CIPPAzDataTableEntity @WebhookTable -Entity $CompleteObject -Force $Results = "Added Audit Log Alert for $($Tenants.count) tenants. It may take up to four hours before Microsoft starts delivering these alerts." - $body = [pscustomobject]@{'Results' = @($results) } # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::OK - Body = $body + Body = @{ 'Results' = @($Results) } }) } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ExecAuditLogSearch.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ExecAuditLogSearch.ps1 index 9df2ffaf6737..dc54f2259f53 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ExecAuditLogSearch.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ExecAuditLogSearch.ps1 @@ -8,6 +8,10 @@ function Invoke-ExecAuditLogSearch { [CmdletBinding()] param($Request, $TriggerMetadata) + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + $Query = $Request.Body if (!$Query.TenantFilter) { Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListAuditLogTest.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListAuditLogTest.ps1 index d51774c11c2e..f7ed737d6a55 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListAuditLogTest.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListAuditLogTest.ps1 @@ -8,6 +8,10 @@ function Invoke-ListAuditLogTest { #> Param($Request, $TriggerMetadata) + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + $AuditLogQuery = @{ TenantFilter = $Request.Query.TenantFilter SearchId = $Request.Query.SearchId diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListWebhookAlert.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListWebhookAlert.ps1 index 42c6bf3e8787..12abc8bd4e6a 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListWebhookAlert.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListWebhookAlert.ps1 @@ -13,8 +13,10 @@ Function Invoke-ListWebhookAlert { $APIName = $Request.Params.CIPPEndpoint $Headers = $Request.Headers Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' - $Table = get-cipptable -TableName 'SchedulerConfig' - $WebhookRow = foreach ($Webhook in Get-CIPPAzDataTableEntity @Table | Where-Object -Property PartitionKey -EQ 'WebhookAlert') { + + # Interact with query parameters or the body of the request. + $Table = Get-CippTable -TableName 'SchedulerConfig' + $WebhookRow = foreach ($Webhook in (Get-CIPPAzDataTableEntity @Table | Where-Object -Property PartitionKey -EQ 'WebhookAlert')) { $Webhook.If = $Webhook.If | ConvertFrom-Json $Webhook.execution = $Webhook.execution | ConvertFrom-Json $Webhook diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-PublicWebhooks.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-PublicWebhooks.ps1 index b2eaf9309790..d0828455b311 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-PublicWebhooks.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-PublicWebhooks.ps1 @@ -8,12 +8,16 @@ function Invoke-PublicWebhooks { #> param($Request, $TriggerMetadata) + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + Set-Location (Get-Item $PSScriptRoot).Parent.FullName $WebhookTable = Get-CIPPTable -TableName webhookTable $WebhookIncoming = Get-CIPPTable -TableName WebhookIncoming $Webhooks = Get-CIPPAzDataTableEntity @WebhookTable Write-Host 'Received request' - $url = ($request.headers.'x-ms-original-url').split('/API') | Select-Object -First 1 + $url = ($Headers.'x-ms-original-url').split('/API') | Select-Object -First 1 $CIPPURL = [string]$url Write-Host $url if ($Webhooks.Resource -eq 'M365AuditLogs') { @@ -21,22 +25,22 @@ function Invoke-PublicWebhooks { $body = 'This webhook is not authorized, its an old entry.' $StatusCode = [HttpStatusCode]::Forbidden } - if ($Request.query.ValidationToken) { + if ($Request.Query.ValidationToken) { Write-Host 'Validation token received - query ValidationToken' - $body = $request.query.ValidationToken + $body = $Request.Query.ValidationToken $StatusCode = [HttpStatusCode]::OK - } elseif ($Request.body.validationCode) { + } elseif ($Request.Body.validationCode) { Write-Host 'Validation token received - body validationCode' - $body = $request.body.validationCode + $body = $Request.Body.validationCode $StatusCode = [HttpStatusCode]::OK - } elseif ($Request.query.validationCode) { + } elseif ($Request.Query.validationCode) { Write-Host 'Validation token received - query validationCode' - $body = $request.query.validationCode + $body = $Request.Query.validationCode $StatusCode = [HttpStatusCode]::OK } elseif ($Request.Query.CIPPID -in $Webhooks.RowKey) { Write-Host 'Found matching CIPPID' - $url = ($request.headers.'x-ms-original-url').split('/API') | Select-Object -First 1 - $Webhookinfo = $Webhooks | Where-Object -Property RowKey -EQ $Request.query.CIPPID + $url = ($Headers.'x-ms-original-url').split('/API') | Select-Object -First 1 + $Webhookinfo = $Webhooks | Where-Object -Property RowKey -EQ $Request.Query.CIPPID if ($Request.Query.Type -eq 'GraphSubscription') { # Graph Subscriptions diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecAppApproval.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecAppApproval.ps1 index 26c9b127959e..42e0b5d0ebf6 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecAppApproval.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecAppApproval.ps1 @@ -18,11 +18,11 @@ Function Invoke-ExecAppApproval { Write-Host "$($Request.query.ID)" # Interact with query parameters or the body of the request. - $applicationid = if ($request.query.applicationid) { $request.query.applicationid } else { $env:ApplicationID } - $Results = get-tenants | ForEach-Object { + $ApplicationId = if ($Request.Query.ApplicationId) { $Request.Query.ApplicationId } else { $env:ApplicationID } + $Results = Get-Tenants | ForEach-Object { [PSCustomObject]@{ defaultDomainName = $_.defaultDomainName - link = "https://login.microsoftonline.com/$($_.customerId)/v2.0/adminconsent?client_id=$applicationid&scope=$applicationid/.default" + link = "https://login.microsoftonline.com/$($_.customerId)/v2.0/adminconsent?client_id=$ApplicationId&scope=$ApplicationId/.default" } } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecAppApprovalTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecAppApprovalTemplate.ps1 index f604328df1c1..9368600072bb 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecAppApprovalTemplate.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecAppApprovalTemplate.ps1 @@ -13,7 +13,7 @@ function Invoke-ExecAppApprovalTemplate { Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' $Table = Get-CIPPTable -TableName 'templates' - $User = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Request.Headers.'x-ms-client-principal')) | ConvertFrom-Json + $User = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Headers.'x-ms-client-principal')) | ConvertFrom-Json $Action = $Request.Query.Action ?? $Request.Body.Action @@ -58,12 +58,12 @@ function Invoke-ExecAppApprovalTemplate { } ) - Write-LogMessage -headers $Request.Headers -API $APIName -message "App Deployment Template Saved: $($Request.Body.TemplateName)" -Sev 'Info' + Write-LogMessage -headers $Headers -API $APIName -message "App Deployment Template Saved: $($Request.Body.TemplateName)" -Sev 'Info' } catch { $Body = @{ 'Results' = $_.Exception.Message } - Write-LogMessage -headers $Request.Headers -API $APIName -message "App Deployment Template Save failed: $($_.Exception.Message)" -Sev 'Error' + Write-LogMessage -headers $Headers -API $APIName -message "App Deployment Template Save failed: $($_.Exception.Message)" -Sev 'Error' } } 'Delete' { @@ -83,7 +83,7 @@ function Invoke-ExecAppApprovalTemplate { $Body = @{ 'Results' = "Successfully deleted template '$TemplateName'" } - Write-LogMessage -headers $Request.Headers -API $APIName -message "App Deployment Template deleted: $TemplateName" -Sev 'Info' + Write-LogMessage -headers $Headers -API $APIName -message "App Deployment Template deleted: $TemplateName" -Sev 'Info' } else { $Body = @{ 'Results' = 'No template found with the provided ID' @@ -93,7 +93,7 @@ function Invoke-ExecAppApprovalTemplate { $Body = @{ 'Results' = "Failed to delete template: $($_.Exception.Message)" } - Write-LogMessage -headers $Request.Headers -API $APIName -message "App Deployment Template Delete failed: $($_.Exception.Message)" -Sev 'Error' + Write-LogMessage -headers $Headers -API $APIName -message "App Deployment Template Delete failed: $($_.Exception.Message)" -Sev 'Error' } } 'Get' { @@ -102,7 +102,7 @@ function Invoke-ExecAppApprovalTemplate { if ($Request.Query.TemplateId) { $templateId = $Request.Query.TemplateId $filter = "PartitionKey eq 'AppApprovalTemplate' and RowKey eq '$templateId'" - Write-LogMessage -headers $Request.Headers -API $APIName -message "Retrieved specific template: $templateId" -Sev 'Info' + Write-LogMessage -headers $Headers -API $APIName -message "Retrieved specific template: $templateId" -Sev 'Info' } $Templates = Get-CIPPAzDataTableEntity @Table -Filter $filter diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecAppPermissionTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecAppPermissionTemplate.ps1 index 87f8185a6628..4c92415403d2 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecAppPermissionTemplate.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecAppPermissionTemplate.ps1 @@ -8,9 +8,13 @@ function Invoke-ExecAppPermissionTemplate { [CmdletBinding()] param($Request, $TriggerMetadata) + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + $Table = Get-CIPPTable -TableName 'AppPermissions' - $User = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Request.Headers.'x-ms-client-principal')) | ConvertFrom-Json + $User = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Headers.'x-ms-client-principal')) | ConvertFrom-Json $Action = $Request.Query.Action ?? $Request.Body.Action @@ -34,7 +38,7 @@ function Invoke-ExecAppPermissionTemplate { 'TemplateId' = $RowKey } } - Write-LogMessage -headers $Request.Headers -API 'ExecAppPermissionTemplate' -message "Permissions Saved for template: $($Request.Body.TemplateName)" -Sev 'Info' -LogData $Permissions + Write-LogMessage -headers $Headers -API 'ExecAppPermissionTemplate' -message "Permissions Saved for template: $($Request.Body.TemplateName)" -Sev 'Info' -LogData $Permissions } catch { Write-Information "Failed to save template: $($_.Exception.Message) - $($_.InvocationInfo.PositionMessage)" $Body = @{ @@ -53,7 +57,7 @@ function Invoke-ExecAppPermissionTemplate { $Body = @{ 'Results' = "Successfully deleted template '$TemplateName'" } - Write-LogMessage -headers $Request.Headers -API 'ExecAppPermissionTemplate' -message "Permission template deleted: $TemplateName" -Sev 'Info' + Write-LogMessage -headers $Headers -API 'ExecAppPermissionTemplate' -message "Permission template deleted: $TemplateName" -Sev 'Info' } else { $Body = @{ 'Results' = 'No Template ID provided for deletion' @@ -71,7 +75,7 @@ function Invoke-ExecAppPermissionTemplate { if ($Request.Query.TemplateId) { $templateId = $Request.Query.TemplateId $filter = "PartitionKey eq 'Templates' and RowKey eq '$templateId'" - Write-LogMessage -headers $Request.Headers -API 'ExecAppPermissionTemplate' -message "Retrieved specific template: $templateId" -Sev 'Info' + Write-LogMessage -headers $Headers -API 'ExecAppPermissionTemplate' -message "Retrieved specific template: $templateId" -Sev 'Info' } $Body = Get-CIPPAzDataTableEntity @Table -Filter $filter | ForEach-Object { diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecAddSPN.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecAddSPN.ps1 index 45baf77621b4..743eb49291ad 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecAddSPN.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecAddSPN.ps1 @@ -15,18 +15,21 @@ Function Invoke-ExecAddSPN { Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' # Interact with query parameters or the body of the request. - $Body = if ($Request.Query.Enable) { '{"accountEnabled":"true"}' } else { '{"accountEnabled":"false"}' } try { - $GraphRequest = New-GraphPostRequest -uri 'https://graph.microsoft.com/v1.0/servicePrincipals' -tenantid $env:TenantID -type POST -Body "{ `"appId`": `"2832473f-ec63-45fb-976f-5d45a7d4bb91`" }" -NoAuthCheck $true - $Results = [pscustomobject]@{'Results' = "Successfully completed request. Add your GDAP migration permissions to your SAM application here: https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/CallAnAPI/appId/$($env:ApplicationID)/isMSAApp/ " } + $null = New-GraphPostRequest -uri 'https://graph.microsoft.com/v1.0/servicePrincipals' -tenantid $env:TenantID -type POST -Body "{ `"appId`": `"2832473f-ec63-45fb-976f-5d45a7d4bb91`" }" -NoAuthCheck $true + $Result = "Successfully completed request. Add your GDAP migration permissions to your SAM application here: https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/CallAnAPI/appId/$($env:ApplicationID)/isMSAApp/ " + $StatusCode = [HttpStatusCode]::OK } catch { - $Results = [pscustomobject]@{'Results' = "Failed to add SPN. Please manually execute 'New-AzureADServicePrincipal -AppId 2832473f-ec63-45fb-976f-5d45a7d4bb91' The error was $($_.Exception.Message)" } + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to add SPN. The error was: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -tenant $env:TenantID -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 = [HttpStatusCode]::OK - Body = $Results + StatusCode = $StatusCode + Body = @{'Results' = $Result } }) } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecOffboardTenant.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecOffboardTenant.ps1 index 4afdfb601df5..6abc2c461279 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecOffboardTenant.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecOffboardTenant.ps1 @@ -9,23 +9,25 @@ Function Invoke-ExecOffboardTenant { #> [CmdletBinding()] param($Request, $TriggerMetadata) + $APIName = $Request.Params.CIPPEndpoint - try { - Write-LogMessage -headers $Request.Headers -API $APINAME -message 'Accessed this API' -Sev 'Debug' + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + try { $TenantQuery = $Request.Body.TenantFilter.value ?? $Request.Body.TenantFilter $Tenant = Get-Tenants -IncludeAll -TenantFilter $TenantQuery $TenantId = $Tenant.customerId $TenantFilter = $Tenant.defaultDomainName - $results = [System.Collections.ArrayList]@() - $errors = [System.Collections.ArrayList]@() + $Results = [System.Collections.Generic.List[object]]::new() + $Errors = [System.Collections.Generic.List[object]]::new() if (!$Tenant) { - $results.Add('Tenant has already been offboarded') + $Results.Add('Tenant has already been offboarded') } elseif ($TenantId -eq $env:TenantID) { - $errors.Add('You cannot offboard the CSP tenant') + $Errors.Add('You cannot offboard the CSP tenant') } else { if ($request.body.RemoveCSPGuestUsers -eq $true) { # Delete guest users who's domains match the CSP tenants @@ -33,9 +35,9 @@ Function Invoke-ExecOffboardTenant { try { $domains = (New-GraphGETRequest -Uri "https://graph.microsoft.com/v1.0/domains?`$select=id" -tenantid $env:TenantID -NoAuthCheck:$true).id $DomainFilter = ($Domains | ForEach-Object { "endswith(mail, '$_')" }) -join ' or ' - $CSPGuestUsers = (New-GraphGETRequest -Uri "https://graph.microsoft.com/v1.0/users?`$select=id,mail&`$filter=userType eq 'Guest' and ($DomainFilter)&`$count=true" -tenantid $Tenantfilter -ComplexFilter) + $CSPGuestUsers = (New-GraphGETRequest -Uri "https://graph.microsoft.com/v1.0/users?`$select=id,mail&`$filter=userType eq 'Guest' and ($DomainFilter)&`$count=true" -tenantid $TenantFilter -ComplexFilter) } catch { - $errors.Add("Failed to retrieve guest users: $($_.Exception.message)") + $Errors.Add("Failed to retrieve guest users: $($_.Exception.message)") } if ($CSPGuestUsers) { @@ -90,29 +92,28 @@ Function Invoke-ExecOffboardTenant { $patchContactBody = if (!($newPropertyContent)) { "{ `"$($property)`" : [] }" } else { [pscustomobject]@{ $property = $newPropertyContent } | ConvertTo-Json } try { - New-GraphPostRequest -type PATCH -body $patchContactBody -Uri "https://graph.microsoft.com/v1.0/organization/$($orgContacts.id)" -tenantid $Tenantfilter -ContentType 'application/json' - $results.Add("Successfully removed notification contacts from $($property): $(($propertyContacts | Where-Object { $domains -contains $_.Split('@')[1] }))") + New-GraphPostRequest -type PATCH -body $patchContactBody -Uri "https://graph.microsoft.com/v1.0/organization/$($orgContacts.id)" -tenantid $TenantFilter -ContentType 'application/json' + $Results.Add("Successfully removed notification contacts from $($property): $(($propertyContacts | Where-Object { $domains -contains $_.Split('@')[1] }))") Write-LogMessage -headers $Request.Headers -API $APIName -message "Contacts were removed from $($property)" -Sev 'Info' -tenant $TenantFilter } catch { - $errors.Add("Failed to update property $($property): $($_.Exception.message)") + $Errors.Add("Failed to update property $($property): $($_.Exception.message)") } } else { $results.Add("No notification contacts found in $($property)") } } - # Add logic for privacyProfile later - rvdwegen + # TODO Add logic for privacyProfile later - rvdwegen } $VendorApps = $Request.Body.vendorApplications if ($VendorApps) { $VendorApps | ForEach-Object { try { - $delete = (New-GraphPostRequest -type 'DELETE' -Uri "https://graph.microsoft.com/v1.0/serviceprincipals/$($_.value)" -tenantid $Tenantfilter) - $results.Add("Successfully removed app $($_.label)") - Write-LogMessage -headers $Request.Headers -API $APIName -message "App $($_.label) was removed" -Sev 'Info' -tenant $TenantFilter + $null = (New-GraphPostRequest -type 'DELETE' -Uri "https://graph.microsoft.com/v1.0/serviceprincipals/$($_.value)" -tenantid $TenantFilter) + $Results.Add("Successfully removed app $($_.label)") + Write-LogMessage -headers $Headers -API $APIName -message "App $($_.label) was removed" -Sev 'Info' -tenant $TenantFilter } catch { - #$results.Add("Failed to removed app $($_.displayName)") - $errors.Add("Failed to removed app $($_.label)") + $Errors.Add("Failed to removed app $($_.label)") } } } @@ -121,21 +122,21 @@ Function Invoke-ExecOffboardTenant { if ($request.body.RemoveMultitenantCSPApps -eq $true) { # Remove multi-tenant apps with the CSP tenant as origin try { - $multitenantCSPApps = (New-GraphGETRequest -Uri "https://graph.microsoft.com/v1.0/servicePrincipals?`$count=true&`$select=displayName,appId,id,appOwnerOrganizationId&`$filter=appOwnerOrganizationId eq $($env:TenantID)" -tenantid $Tenantfilter -ComplexFilter) - $sortedArray = $multitenantCSPApps | Sort-Object @{Expression = { if ($_.appId -eq $env:ApplicationID) { 1 } else { 0 } }; Ascending = $true } + $MultiTenantCSPApps = (New-GraphGETRequest -Uri "https://graph.microsoft.com/v1.0/servicePrincipals?`$count=true&`$select=displayName,appId,id,appOwnerOrganizationId&`$filter=appOwnerOrganizationId eq $($env:TenantID)" -tenantid $TenantFilter -ComplexFilter) + $sortedArray = $MultiTenantCSPApps | Sort-Object @{Expression = { if ($_.appId -eq $env:ApplicationID) { 1 } else { 0 } }; Ascending = $true } $sortedArray | ForEach-Object { try { - $delete = (New-GraphPostRequest -type 'DELETE' -Uri "https://graph.microsoft.com/v1.0/serviceprincipals/$($_.id)" -tenantid $Tenantfilter) - $results.Add("Successfully removed app $($_.displayName)") + $null = (New-GraphPostRequest -type 'DELETE' -Uri "https://graph.microsoft.com/v1.0/serviceprincipals/$($_.id)" -tenantid $TenantFilter) + $Results.Add("Successfully removed app $($_.displayName)") Write-LogMessage -headers $Request.Headers -API $APIName -message "App $($_.displayName) was removed" -Sev 'Info' -tenant $TenantFilter } catch { - #$results.Add("Failed to removed app $($_.displayName)") - $errors.Add("Failed to removed app $($_.displayName)") + #$Results.Add("Failed to removed app $($_.displayName)") + $Errors.Add("Failed to removed app $($_.displayName)") } } } catch { - #$results.Add("Failed to retrieve multitenant apps, no apps have been removed: $($_.Exception.message)") - $errors.Add("Failed to retrieve multitenant CSP apps, no apps have been removed: $($_.Exception.message)") + #$Results.Add("Failed to retrieve multi-tenant apps, no apps have been removed: $($_.Exception.message)") + $Errors.Add("Failed to retrieve multi-tenant CSP apps, no apps have been removed: $($_.Exception.message)") } } $ClearCache = $false @@ -146,8 +147,8 @@ Function Invoke-ExecOffboardTenant { $delegatedAdminRelationships = (New-GraphGETRequest -Uri "https://graph.microsoft.com/v1.0/tenantRelationships/delegatedAdminRelationships?`$filter=(status eq 'active') AND (customer/tenantId eq '$tenantid')" -tenantid $env:TenantID) $delegatedAdminRelationships | ForEach-Object { try { - $terminate = (New-GraphPostRequest -type 'POST' -Uri "https://graph.microsoft.com/v1.0/tenantRelationships/delegatedAdminRelationships/$($_.id)/requests" -body '{"action":"terminate"}' -ContentType 'application/json' -tenantid $env:TenantID) - $results.Add("Successfully terminated GDAP relationship $($_.displayName) from tenant $TenantFilter") + $null = (New-GraphPostRequest -type 'POST' -Uri "https://graph.microsoft.com/v1.0/tenantRelationships/delegatedAdminRelationships/$($_.id)/requests" -body '{"action":"terminate"}' -ContentType 'application/json' -tenantid $env:TenantID) + $Results.Add("Successfully terminated GDAP relationship $($_.displayName) from tenant $TenantFilter") Write-LogMessage -headers $Request.Headers -API $APIName -message "GDAP Relationship $($_.displayName) has been terminated" -Sev 'Info' -tenant $TenantFilter } catch { @@ -158,20 +159,20 @@ Function Invoke-ExecOffboardTenant { } } catch { $($_.Exception.message) - #$results.Add("Failed to retrieve GDAP relationships, no relationships have been terminated: $($_.Exception.message)") - $errors.Add("Failed to retrieve GDAP relationships, no relationships have been terminated: $($_.Exception.message)") + #$Results.Add("Failed to retrieve GDAP relationships, no relationships have been terminated: $($_.Exception.message)") + $Errors.Add("Failed to retrieve GDAP relationships, no relationships have been terminated: $($_.Exception.message)") } } if ($request.body.TerminateContract -eq $true) { # Terminate contract relationship try { - $terminate = (New-GraphPostRequest -type 'PATCH' -body '{ "relationshipToPartner": "none" }' -Uri "https://api.partnercenter.microsoft.com/v1/customers/$TenantFilter" -ContentType 'application/json' -scope 'https://api.partnercenter.microsoft.com/user_impersonation' -tenantid $env:TenantID) - $results.Add('Successfully terminated contract relationship') - Write-LogMessage -headers $Request.Headers -API $APIName -message 'Contract relationship terminated' -Sev 'Info' -tenant $TenantFilter + $null = (New-GraphPostRequest -type 'PATCH' -body '{ "relationshipToPartner": "none" }' -Uri "https://api.partnercenter.microsoft.com/v1/customers/$TenantFilter" -ContentType 'application/json' -scope 'https://api.partnercenter.microsoft.com/user_impersonation' -tenantid $env:TenantID) + $Results.Add('Successfully terminated contract relationship') + Write-LogMessage -headers $Headers -API $APIName -message 'Contract relationship terminated' -Sev 'Info' -tenant $TenantFilter } catch { - #$results.Add("Failed to terminate contract relationship: $($_.Exception.message)") - $errors.Add("Failed to terminate contract relationship: $($_.Exception.message)") + #$Results.Add("Failed to terminate contract relationship: $($_.Exception.message)") + $Errors.Add("Failed to terminate contract relationship: $($_.Exception.message)") } } } @@ -181,11 +182,11 @@ Function Invoke-ExecOffboardTenant { $Results.Add('Tenant cache has been cleared') } - Write-LogMessage -headers $Request.Headers -API $APIName -message 'Offboarding completed' -Sev 'Info' -tenant $TenantFilter + Write-LogMessage -headers $Headers -API $APIName -message 'Offboarding completed' -Sev 'Info' -tenant $TenantFilter $StatusCode = [HttpStatusCode]::OK $body = [pscustomobject]@{ - 'Results' = @($results) - 'Errors' = @($errors) + 'Results' = @($Results) + 'Errors' = @($Errors) } } catch { $StatusCode = [HttpStatusCode]::OK diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecOnboardTenant.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecOnboardTenant.ps1 index ad61daf3ddd2..fdf1b97e79ba 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecOnboardTenant.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecOnboardTenant.ps1 @@ -9,8 +9,11 @@ function Invoke-ExecOnboardTenant { #> param($Request, $TriggerMetadata) - $APIName = 'ExecOnboardTenant' - Write-LogMessage -headers $Request.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' + + # Interact with query parameters or the body of the request. $Id = $Request.Body.id if ($Id) { try { @@ -84,7 +87,7 @@ function Invoke-ExecOnboardTenant { Batch = @($Item) } $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) - Write-LogMessage -headers $Request.Headers -API $APINAME -message "Onboarding job $Id started" -Sev 'Info' -LogData @{ 'InstanceId' = $InstanceId } + Write-LogMessage -headers $Headers -API $APIName -message "Onboarding job $Id started" -Sev 'Info' -LogData @{ 'InstanceId' = $InstanceId } } $Steps = $TenantOnboarding.OnboardingSteps | ConvertFrom-Json diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecUpdateSecureScore.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecUpdateSecureScore.ps1 index 406a029c49c4..bf38d341040c 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecUpdateSecureScore.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecUpdateSecureScore.ps1 @@ -15,22 +15,29 @@ Function Invoke-ExecUpdateSecureScore { Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' # Interact with query parameters or the body of the request. + $TenantFilter = $Request.Body.TenantFilter + $ControlName = $Request.Body.ControlName $Body = @{ - comment = $request.body.reason - state = $request.body.resolutionType.value - vendorInformation = $request.body.vendorInformation + comment = $Request.Body.reason + state = $Request.Body.resolutionType.value + vendorInformation = $Request.Body.vendorInformation } try { - $GraphRequest = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/security/secureScoreControlProfiles/$($Request.body.ControlName)" -tenantid $Request.body.TenantFilter -type PATCH -Body $($Body | ConvertTo-Json -Compress) - $Results = [pscustomobject]@{'Results' = "Successfully set control to $($Body.state) " } + $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/security/secureScoreControlProfiles/$ControlName" -tenantid $TenantFilter -type PATCH -Body (ConvertTo-Json -InputObject $Body -Compress) + $StatusCode = [HttpStatusCode]::OK + $Result = "Successfully set control $ControlName to $($Body.state)" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev 'Info' } catch { - $Results = [pscustomobject]@{'Results' = "Failed to set Control to $($Body.state) $($_.Exception.Message)" } + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to set control $ControlName to $($Body.state). Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -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 = [HttpStatusCode]::OK - Body = $Results + StatusCode = $StatusCode + Body = @{'Results' = $Result } }) } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ListAppConsentRequests.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ListAppConsentRequests.ps1 index 55f5fdd82b76..89472cd5821a 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ListAppConsentRequests.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ListAppConsentRequests.ps1 @@ -10,14 +10,15 @@ function Invoke-ListAppConsentRequests { param($Request, $TriggerMetadata) $APIName = $Request.Params.CIPPEndpoint - $TenantFilter = $Request.Query.TenantFilter - Write-LogMessage -headers $Request.Headers -API $APINAME -message 'Accessed this API' -Sev 'Debug' + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + + # Interact with query parameters or the body of the request. + $TenantFilter = $Request.Query.tenantFilter try { - if ($Request.Query.TenantFilter -eq 'AllTenants') { + if ($TenantFilter -eq 'AllTenants') { throw 'AllTenants is not yet supported' - } else { - $TenantFilter = $Request.Query.TenantFilter } $appConsentRequests = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/identityGovernance/appConsent/appConsentRequests' -tenantid $TenantFilter # Need the beta endpoint to get consentType @@ -49,9 +50,10 @@ function Invoke-ListAppConsentRequests { } $StatusCode = [HttpStatusCode]::OK } catch { - $StatusCode = [HttpStatusCode]::OK - Write-LogMessage -Headers $Headers -API $APIName -message 'app consent request list failed' -Sev 'Error' -tenant $TenantFilter - $Results = @{ appDisplayName = "Error: $($_.Exception.Message)" } + $ErrorMessage = Get-CippException -Exception $_ + $StatusCode = [HttpStatusCode]::InternalServerError + Write-LogMessage -Headers $Headers -API $APIName -message 'app consent request list failed' -Sev 'Error' -tenant $TenantFilter -LogData $ErrorMessage + $Results = @{ appDisplayName = "Error: $($ErrorMessage.NormalizedError)" } } Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ListDomains.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ListDomains.ps1 index ece424ca883d..cf11e6b09050 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ListDomains.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ListDomains.ps1 @@ -14,24 +14,20 @@ Function Invoke-ListDomains { $Headers = $Request.Headers Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' - - - # Interact with query parameters or the body of the request. - $TenantFilter = $Request.Query.TenantFilter + $TenantFilter = $Request.Query.tenantFilter try { - $GraphRequest = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/domains' -tenantid $TenantFilter | Select-Object id, isdefault, isinitial | Sort-Object isdefault -Descending + $Result = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/domains' -tenantid $TenantFilter | Select-Object id, isdefault, isinitial | Sort-Object isdefault -Descending $StatusCode = [HttpStatusCode]::OK } catch { - $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - $StatusCode = [HttpStatusCode]::Forbidden - $GraphRequest = $ErrorMessage + $Result = Get-NormalizedError -Message $_.Exception.Message + $StatusCode = [HttpStatusCode]::InternalServerError } # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ StatusCode = $StatusCode - Body = @($GraphRequest) + Body = @($Result) }) } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ListTenantOnboarding.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ListTenantOnboarding.ps1 index 843248a2d2ff..ce5bafeedcd3 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ListTenantOnboarding.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ListTenantOnboarding.ps1 @@ -10,7 +10,7 @@ function Invoke-ListTenantOnboarding { $APIName = $Request.Params.CIPPEndpoint $Headers = $Request.Headers - Write-LogMessage -headers $Headers -API $APINAME -message 'Accessed this API' -Sev 'Debug' + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' try { diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-SetAuthMethod.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-SetAuthMethod.ps1 index 9e089ff3f77c..5c5d3c2839b3 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-SetAuthMethod.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-SetAuthMethod.ps1 @@ -21,13 +21,13 @@ function Invoke-SetAuthMethod { $Result = Set-CIPPAuthenticationPolicy -Tenant $TenantFilter -APIName $APIName -AuthenticationMethodId $AuthenticationMethodId -Enabled $State -Headers $Headers $StatusCode = [HttpStatusCode]::OK } catch { - $Result = $_ - $StatusCode = [HttpStatusCode]::Forbidden + $Result = $_.Exception.Message + $StatusCode = [HttpStatusCode]::InternalServerError } # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ StatusCode = $StatusCode - Body = [pscustomobject]@{'Results' = "$Result" } + Body = [pscustomobject]@{'Results' = $Result } }) } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-AddTenant.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-AddTenant.ps1 index 65a355bf337f..1234c4523d2d 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-AddTenant.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-AddTenant.ps1 @@ -9,7 +9,10 @@ function Invoke-AddTenant { param($Request, $TriggerMetadata) $APIName = $Request.Params.CIPPEndpoint - Write-LogMessage -headers $Request.Headers -API $APINAME -message 'Accessed this API' -Sev 'Debug' + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + + # Interact with query parameters or the body of the request. $Action = $Request.Body.Action ?? $Request.Query.Action $TenantName = $Request.Body.TenantName ?? $Request.Query.TenantName $StatusCode = [HttpStatusCode]::OK diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-EditTenant.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-EditTenant.ps1 index b0fa498d20ed..e18c9ba35954 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-EditTenant.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-EditTenant.ps1 @@ -12,9 +12,9 @@ function Invoke-EditTenant { $APIName = $Request.Params.CIPPEndpoint $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' - Write-LogMessage -headers $Headers -API $APINAME -message 'Accessed this API' -Sev 'Debug' - + # Interact with query parameters or the body of the request. $customerId = $Request.Body.customerId $tenantAlias = $Request.Body.tenantAlias $tenantGroups = $Request.Body.tenantGroups diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenantDetails.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenantDetails.ps1 index 786a2e4cb673..8f3bbccc0063 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenantDetails.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenantDetails.ps1 @@ -11,37 +11,34 @@ Function Invoke-ListTenantDetails { param($Request, $TriggerMetadata) $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' - Write-LogMessage -headers $Request.Headers -API $APINAME -message 'Accessed this API' -Sev 'Debug' - - $tenantfilter = $Request.Query.TenantFilter + # Interact with query parameters or the body of the request. + $TenantFilter = $Request.Query.tenantFilter try { - $org = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/organization' -tenantid $tenantfilter | Select-Object displayName, id, city, country, countryLetterCode, street, state, postalCode, + $org = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/organization' -tenantid $TenantFilter | Select-Object displayName, id, city, country, countryLetterCode, street, state, postalCode, @{ Name = 'businessPhones'; Expression = { $_.businessPhones -join ', ' } }, @{ Name = 'technicalNotificationMails'; Expression = { $_.technicalNotificationMails -join ', ' } }, tenantType, createdDateTime, onPremisesLastPasswordSyncDateTime, onPremisesLastSyncDateTime, onPremisesSyncEnabled, assignedPlans - $customProperties = Get-TenantProperties -customerId $tenantfilter + $customProperties = Get-TenantProperties -customerId $TenantFilter $org | Add-Member -MemberType NoteProperty -Name 'customProperties' -Value $customProperties - $Groups = (Get-TenantGroups -TenantFilter $tenantfilter) ?? @() + $Groups = (Get-TenantGroups -TenantFilter $TenantFilter) ?? @() $org | Add-Member -MemberType NoteProperty -Name 'Groups' -Value @($Groups) - - # Respond with the successful output - Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ - StatusCode = [HttpStatusCode]::OK - Body = $org - }) } catch { - # Log the exception message - Write-LogMessage -headers $Request.Headers -API $APINAME -message "Error: $($_.Exception.Message)" -Sev 'Error' - - # Respond with a 500 error and include the exception message in the response body - Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ - StatusCode = [HttpStatusCode]::InternalServerError - Body = Get-NormalizedError -message $_.Exception.Message - }) + $ErrorMessage = Get-CippException -Exception $_ + $org = "Failed to retrieve tenant details: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -message $org -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 = $org + }) } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenants.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenants.ps1 index d91f6d5a2392..624e16be31c7 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenants.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenants.ps1 @@ -11,8 +11,10 @@ function Invoke-ListTenants { param($Request, $TriggerMetadata) $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' - Write-LogMessage -headers $Request.Headers -API $APINAME -message 'Accessed this API' -Sev 'Debug' + # Interact with query parameters or the body of the request. $TenantAccess = Test-CIPPAccess -Request $Request -TenantList Write-Host "Tenant Access: $TenantAccess" @@ -67,7 +69,7 @@ function Invoke-ListTenants { } } try { - $tenantfilter = $Request.Query.TenantFilter + $TenantFilter = $Request.Query.tenantFilter $Tenants = Get-Tenants -IncludeErrors -SkipDomains if ($TenantAccess -notcontains 'AllTenants') { $Tenants = $Tenants | Where-Object -Property customerId -In $TenantAccess @@ -107,12 +109,12 @@ function Invoke-ListTenants { } } else { - $body = $Tenants | Where-Object -Property defaultDomainName -EQ $Tenantfilter + $body = $Tenants | Where-Object -Property defaultDomainName -EQ $TenantFilter } - Write-LogMessage -headers $Request.Headers -tenant $Tenantfilter -API $APINAME -message 'Listed Tenant Details' -Sev 'Debug' + Write-LogMessage -headers $Headers -tenant $TenantFilter -API $APIName -message 'Listed Tenant Details' -Sev 'Debug' } catch { - Write-LogMessage -headers $Request.Headers -tenant $Tenantfilter -API $APINAME -message "List Tenant failed. The error is: $($_.Exception.Message)" -Sev 'Error' + Write-LogMessage -headers $Headers -tenant $TenantFilter -API $APIName -message "List Tenant failed. The error is: $($_.Exception.Message)" -Sev 'Error' $body = [pscustomobject]@{ 'Results' = "Failed to retrieve tenants: $($_.Exception.Message)" defaultDomainName = '' diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListCSPsku.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListCSPsku.ps1 index dcf8fb9a523f..ff61308e7023 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListCSPsku.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListCSPsku.ps1 @@ -24,15 +24,17 @@ Function Invoke-ListCSPsku { } else { $GraphRequest = Get-SherwebCatalog -TenantFilter $TenantFilter } + $StatusCode = [HttpStatusCode]::OK } catch { $GraphRequest = [PSCustomObject]@{ name = @(@{value = 'Error getting catalog' }) sku = $_.Exception.Message } + $StatusCode = [HttpStatusCode]::InternalServerError } Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ - StatusCode = [HttpStatusCode]::OK + StatusCode = $StatusCode Body = @($GraphRequest) }) -Clobber diff --git a/Modules/CIPPCore/Public/Get-CIPPPerUserMFA.ps1 b/Modules/CIPPCore/Public/Get-CIPPPerUserMFA.ps1 index 0187ef3bda0f..28ecc040f452 100644 --- a/Modules/CIPPCore/Public/Get-CIPPPerUserMFA.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPPerUserMFA.ps1 @@ -2,23 +2,23 @@ function Get-CIPPPerUserMFA { [CmdletBinding()] param( $TenantFilter, - $userId, + $UserId, $Headers, $AllUsers = $false ) try { if ($AllUsers -eq $true) { - $AllUsers = New-graphGetRequest -Uri "https://graph.microsoft.com/v1.0/users?`$top=999&`$select=UserPrincipalName,Id,perUserMfaState" -tenantid $tenantfilter + $AllUsers = New-GraphGetRequest -Uri "https://graph.microsoft.com/v1.0/users?`$top=999&`$select=UserPrincipalName,Id,perUserMfaState" -tenantid $TenantFilter return $AllUsers } else { - $MFAState = New-graphGetRequest -Uri "https://graph.microsoft.com/v1.0/users/$($userId)?`$select=UserPrincipalName,Id,perUserMfaState" -tenantid $tenantfilter + $MFAState = New-GraphGetRequest -Uri "https://graph.microsoft.com/v1.0/users/$($UserId)?`$select=UserPrincipalName,Id,perUserMfaState" -tenantid $TenantFilter return [PSCustomObject]@{ PerUserMFAState = $MFAState.perUserMfaState - UserPrincipalName = $userId + UserPrincipalName = $UserId } } } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - "Failed to get MFA State for $id : $ErrorMessage" + throw "Failed to get MFA State for $UserId : $ErrorMessage" } } diff --git a/Modules/CIPPCore/Public/New-CIPPOneDriveShortCut.ps1 b/Modules/CIPPCore/Public/New-CIPPOneDriveShortCut.ps1 index a7ce98992420..29764fc40e34 100644 --- a/Modules/CIPPCore/Public/New-CIPPOneDriveShortCut.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPOneDriveShortCut.ps1 @@ -2,21 +2,21 @@ function New-CIPPOneDriveShortCut { [CmdletBinding()] param ( - $username, - $userid, + $Username, + $UserId, $URL, $TenantFilter, $APIName = 'Create OneDrive shortcut', $Headers ) - Write-Host "Received $username and $userid. We're using $url and $TenantFilter" + Write-Host "Received $Username and $UserId. We're using $URL and $TenantFilter" try { - $SiteInfo = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/sites/' -tenantid $TenantFilter -asapp $true | Where-Object -Property weburl -EQ $url + $SiteInfo = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/sites/' -tenantid $TenantFilter -asapp $true | Where-Object -Property weburl -EQ $URL $ListItemUniqueId = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/sites/$($siteInfo.id)/drive?`$select=SharepointIds" -tenantid $TenantFilter -asapp $true).SharePointIds $body = [PSCustomObject]@{ name = 'Documents' remoteItem = @{ - sharepointIds = @{ + SharepointIds = @{ listId = $($ListItemUniqueId.listid) listItemUniqueId = 'root' siteId = $($ListItemUniqueId.siteId) @@ -28,11 +28,12 @@ function New-CIPPOneDriveShortCut { } | ConvertTo-Json -Depth 10 New-GraphPOSTRequest -method POST "https://graph.microsoft.com/beta/users/$username/drive/root/children" -body $body -tenantid $TenantFilter -asapp $true Write-LogMessage -API $APIName -headers $Headers -message "Created OneDrive shortcut called $($SiteInfo.displayName) for $($username)" -Sev 'info' - return "Created OneDrive Shortcut for $username called $($SiteInfo.displayName) " + return "Successfully created OneDrive Shortcut for $username called $($SiteInfo.displayName) " } catch { $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -headers $Headers -API $APIName -message "Could not add Onedrive shortcut to $username : $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage - return "Could not add Onedrive shortcut to $username : $($ErrorMessage.NormalizedError)" + $Result = "Could not add Onedrive shortcut to $username : $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Error' -LogData $ErrorMessage + throw $Result } } diff --git a/Modules/CIPPCore/Public/New-CIPPSharepointSite.ps1 b/Modules/CIPPCore/Public/New-CIPPSharepointSite.ps1 index 9cf2c95c301f..6a37dbbf088a 100644 --- a/Modules/CIPPCore/Public/New-CIPPSharepointSite.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPSharepointSite.ps1 @@ -60,7 +60,10 @@ function New-CIPPSharepointSite { [string]$Classification, [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + + $APIName = 'Create SharePoint Site', + $Headers ) $tenantName = (New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/sites/root' -asApp $true -tenantid $TenantFilter).id.Split('.')[0] $AdminUrl = "https://$($tenantName)-admin.sharepoint.com" @@ -142,11 +145,35 @@ function New-CIPPSharepointSite { $Results = New-GraphPostRequest -scope "$AdminUrl/.default" -uri "$AdminUrl/_api/SPSiteManager/create" -Body ($body | ConvertTo-Json -Compress -Depth 10) -tenantid $TenantFilter -ContentType 'application/json' -AddedHeaders $AddedHeaders } - if ($Results.SiteStatus -eq '4') { - return 'This site already exists. Please choose a different site name.' - } - if ($Results.SiteStatus -eq '2') { - return "The site $($SiteName) was created successfully." + # Check the results. This response is weird. https://learn.microsoft.com/en-us/sharepoint/dev/apis/site-creation-rest + switch ($Results.SiteStatus) { + '0' { + $Result = "Failed to create new SharePoint site $SiteName with URL $SiteUrl. The site doesn't exist." + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -sev Error + throw $Results + } + '1' { + $Result = "Successfully created new SharePoint site $SiteName with URL $SiteUrl. The site is however currently being provisioned. Please wait for it to finish." + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -sev Info + return $Results + } + '2' { + $Result = "Successfully created new SharePoint site $SiteName with URL $SiteUrl" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -sev Info + return $Results + } + '3' { + $Result = "Failed to create new SharePoint site $SiteName with URL $SiteUrl. An error occurred while provisioning the site." + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -sev Error + throw $Results + } + '4' { + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -sev Error + $Result = "Failed to create new SharePoint site $SiteName with URL $SiteUrl. The site already exists." + throw $Result + } + Default {} } + } diff --git a/Modules/CIPPCore/Public/Remove-CIPPGroupMember.ps1 b/Modules/CIPPCore/Public/Remove-CIPPGroupMember.ps1 index 862b4b318610..c723ff2d77e7 100644 --- a/Modules/CIPPCore/Public/Remove-CIPPGroupMember.ps1 +++ b/Modules/CIPPCore/Public/Remove-CIPPGroupMember.ps1 @@ -7,23 +7,23 @@ function Remove-CIPPGroupMember( [string]$APIName = 'Remove Group Member' ) { try { - if ($member -like '*#EXT#*') { $member = [System.Web.HttpUtility]::UrlEncode($member) } - # $MemberIDs = 'https://graph.microsoft.com/v1.0/directoryObjects/' + (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$($member)" -tenantid $TenantFilter).id - # $addmemberbody = "{ `"members@odata.bind`": $(ConvertTo-Json @($MemberIDs)) }" + if ($Member -like '*#EXT#*') { $Member = [System.Web.HttpUtility]::UrlEncode($Member) } + # $MemberIDs = 'https://graph.microsoft.com/v1.0/directoryObjects/' + (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$($Member)" -tenantid $TenantFilter).id + # $AddMemberBody = "{ `"members@odata.bind`": $(ConvertTo-Json @($MemberIDs)) }" if ($GroupType -eq 'Distribution list' -or $GroupType -eq 'Mail-Enabled Security') { - $Params = @{ Identity = $GroupId; Member = $member; BypassSecurityGroupManagerCheck = $true } - New-ExoRequest -tenantid $TenantFilter -cmdlet 'Remove-DistributionGroupMember' -cmdParams $params -UseSystemMailbox $true + $Params = @{ Identity = $GroupId; Member = $Member; BypassSecurityGroupManagerCheck = $true } + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Remove-DistributionGroupMember' -cmdParams $Params -UseSystemMailbox $true } else { - New-GraphPostRequest -uri "https://graph.microsoft.com/beta/groups/$($GroupId)/members/$($Member)/`$ref" -tenantid $TenantFilter -type DELETE -body '{}' -Verbose + $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/groups/$($GroupId)/members/$($Member)/`$ref" -tenantid $TenantFilter -type DELETE -body '{}' -Verbose } - $Message = "Successfully removed user $($Member) from $($GroupId)." - Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Message -Sev 'Info' - return $message + $Results = "Successfully removed user $($Member) from $($GroupId)." + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Results -Sev Info + return $Results } catch { $ErrorMessage = Get-CippException -Exception $_ - $message = "Failed to remove user $($Member) from $($GroupId): $($ErrorMessage.NormalizedError)" - Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $message -Sev 'error' -LogData $ErrorMessage - return $message + $Results = "Failed to remove user $($Member) from $($GroupId): $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Results -Sev Error -LogData $ErrorMessage + throw $Results } } diff --git a/Modules/CIPPCore/Public/Request-CIPPSPOPersonalSite.ps1 b/Modules/CIPPCore/Public/Request-CIPPSPOPersonalSite.ps1 index 46514ac6dd14..d3707829cc0f 100644 --- a/Modules/CIPPCore/Public/Request-CIPPSPOPersonalSite.ps1 +++ b/Modules/CIPPCore/Public/Request-CIPPSPOPersonalSite.ps1 @@ -43,10 +43,11 @@ function Request-CIPPSPOPersonalSite { $Request = New-GraphPostRequest -scope "$AdminURL/.default" -tenantid $TenantFilter -Uri "$AdminURL/_vti_bin/client.svc/ProcessQuery" -Type POST -Body $XML -ContentType 'text/xml' if (!$Request.IsComplete) { throw } Write-LogMessage -headers $Headers -API $APIName -message "Requested personal site for $($UserEmails -join ', ')" -Sev 'Info' -tenant $TenantFilter - return "Requested personal site for $($UserEmails -join ', ')" + return "Successfully requested personal site for $($UserEmails -join ', ')" } catch { $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -headers $Headers -API $APIName -message "Could not request personal site for $($UserEmails -join ', '). Error: $($ErrorMessage.NormalizedError)" -Sev 'Error' -tenant $TenantFilter -LogData $ErrorMessage - return "Could not request personal site for $($UserEmails -join ', '). Error: $($ErrorMessage.NormalizedError)" + $Result = "Failed to request personal site for $($UserEmails -join ', '). Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Error' -tenant $TenantFilter -LogData $ErrorMessage + throw $Result } } diff --git a/Modules/CIPPCore/Public/Revoke-CIPPSessions.ps1 b/Modules/CIPPCore/Public/Revoke-CIPPSessions.ps1 index a7f37e6e7854..e9d319008a2d 100644 --- a/Modules/CIPPCore/Public/Revoke-CIPPSessions.ps1 +++ b/Modules/CIPPCore/Public/Revoke-CIPPSessions.ps1 @@ -15,7 +15,9 @@ function Revoke-CIPPSessions { } catch { $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -headers $Headers -API $APIName -message "Failed to revoke sessions for $($username): $($ErrorMessage.NormalizedError)" -Sev 'Error' -tenant $TenantFilter -LogData $ErrorMessage - return "Revoke Session Failed: $($ErrorMessage.NormalizedError)" + $Result = "Failed to revoke sessions for $($username). Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Error' -tenant $TenantFilter -LogData $ErrorMessage + # TODO - needs to be changed to throw, but the rest of the functions using this cant handle anything but a return. + return $Result } } diff --git a/Modules/CIPPCore/Public/Set-CIPPResetPassword.ps1 b/Modules/CIPPCore/Public/Set-CIPPResetPassword.ps1 index ff87694f31f0..69b780169c69 100644 --- a/Modules/CIPPCore/Public/Set-CIPPResetPassword.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPResetPassword.ps1 @@ -29,14 +29,16 @@ function Set-CIPPResetPassword { Write-LogMessage -headers $Headers -API $APIName -message "Reset the password for $DisplayName, $($UserID). User must change password is set to $forceChangePasswordNextSignIn" -Sev 'Info' -tenant $TenantFilter if ($UserDetails.onPremisesSyncEnabled -eq $true) { - return [pscustomobject]@{ resultText = "Reset the password for $DisplayName, $($UserID). User must change password is set to $forceChangePasswordNextSignIn. The new password is $password. WARNING: This user is AD synced. Please confirm passthrough or writeback is enabled." - copyField = $password - state = 'warning' + return [pscustomobject]@{ + resultText = "Reset the password for $DisplayName, $($UserID). User must change password is set to $forceChangePasswordNextSignIn. The new password is $password. WARNING: This user is AD synced. Please confirm passthrough or writeback is enabled." + copyField = $password + state = 'warning' } } else { - return [pscustomobject]@{ resultText = "Reset the password for $DisplayName, $($UserID). User must change password is set to $forceChangePasswordNextSignIn. The new password is $password" - copyField = $password - state = 'success' + return [pscustomobject]@{ + resultText = "Reset the password for $DisplayName, $($UserID). User must change password is set to $forceChangePasswordNextSignIn. The new password is $password" + copyField = $password + state = 'success' } } } catch { diff --git a/cspell.json b/cspell.json index 0684ca9353ee..74367cb934af 100644 --- a/cspell.json +++ b/cspell.json @@ -14,19 +14,26 @@ "Connectwise", "CPIM", "Datto", + "endswith", + "entra", "Entra", + "gdap", "GDAP", "IMAP", "Intune", + "locationcipp", "MAPI", + "Multitenant", "OBEE", "passwordless", "Passwordless", "PSTN", + "rvdwegen", + "sharepoint", + "SharePoint", "Sherweb", "Signup", "SSPR", - "SharePoint", "Standardcal", "Terrl", "TNEF", From 40b91b39e02ec35f7bc61124833934b78070ba6a Mon Sep 17 00:00:00 2001 From: Jack Date: Tue, 10 Jun 2025 16:54:02 +0100 Subject: [PATCH 069/160] Enhance logging and permission handling in Invoke-ExecModifyCalPerms function by including FolderName in log messages and updating permission commands to use dynamic folder names. --- .../Administration/Invoke-ExecModifyCalPerms.ps1 | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) 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 e2eefeff06ce..c25af0673c14 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 @@ -64,8 +64,9 @@ Function Invoke-ExecModifyCalPerms { $PermissionLevel = $Permission.PermissionLevel.value ?? $Permission.PermissionLevel $Modification = $Permission.Modification $CanViewPrivateItems = $Permission.CanViewPrivateItems ?? $false + $FolderName = $Permission.FolderName ?? 'Calendar' - Write-LogMessage -headers $Request.Headers -API $APINAME-message "Permission Level: $PermissionLevel, Modification: $Modification, CanViewPrivateItems: $CanViewPrivateItems" -Sev 'Debug' + Write-LogMessage -headers $Request.Headers -API $APINAME-message "Permission Level: $PermissionLevel, Modification: $Modification, CanViewPrivateItems: $CanViewPrivateItems, FolderName: $FolderName" -Sev 'Debug' # Handle UserID as array or single value $TargetUsers = @($Permission.UserID | ForEach-Object { $_.value ?? $_ }) @@ -79,11 +80,11 @@ Function Invoke-ExecModifyCalPerms { if ($Modification -eq 'Remove') { try { $CalPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Remove-MailboxFolderPermission' -cmdParams @{ - Identity = "$($userid):\Calendar" + Identity = "$($userid):\$FolderName" User = $TargetUser Confirm = $false } - $null = $results.Add("Removed $($TargetUser) from $($username) Calendar permissions") + $null = $results.Add("Removed $($TargetUser) from $($username) $FolderName permissions") } catch { $null = $results.Add("No existing permissions to remove for $($TargetUser)") @@ -93,7 +94,7 @@ Function Invoke-ExecModifyCalPerms { Write-LogMessage -headers $Request.Headers -API $APINAME-message "Setting permissions with AccessRights: $PermissionLevel" -Sev 'Debug' $cmdParams = @{ - Identity = "$($userid):\Calendar" + Identity = "$($userid):\$FolderName" User = $TargetUser AccessRights = $PermissionLevel Confirm = $false @@ -106,12 +107,12 @@ Function Invoke-ExecModifyCalPerms { try { # Try Add first $CalPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Add-MailboxFolderPermission' -cmdParams $cmdParams - $null = $results.Add("Granted $($TargetUser) $($PermissionLevel) access to $($username) Calendar$($CanViewPrivateItems ? ' with access to private items' : '')") + $null = $results.Add("Granted $($TargetUser) $($PermissionLevel) access to $($username) $FolderName$($CanViewPrivateItems ? ' with access to private items' : '')") } catch { # If Add fails, try Set $CalPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Set-MailboxFolderPermission' -cmdParams $cmdParams - $null = $results.Add("Updated $($TargetUser) $($PermissionLevel) access to $($username) Calendar$($CanViewPrivateItems ? ' with access to private items' : '')") + $null = $results.Add("Updated $($TargetUser) $($PermissionLevel) access to $($username) $FolderName$($CanViewPrivateItems ? ' with access to private items' : '')") } } Write-LogMessage -headers $Request.Headers -API $APINAME-message "Successfully executed $($PermissionLevel) permission modification for $($TargetUser) on $($username)" -Sev 'Info' -tenant $TenantFilter From 431d1f5285c7f2d199b8b12eda2ca3a8f7854702 Mon Sep 17 00:00:00 2001 From: Jack Date: Tue, 10 Jun 2025 17:09:27 +0100 Subject: [PATCH 070/160] Refactored to use Set-CIPPCalendarPermission --- .../Invoke-ExecModifyCalPerms.ps1 | 50 +++++-------------- 1 file changed, 12 insertions(+), 38 deletions(-) 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 c25af0673c14..9d662bc4533d 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 @@ -77,44 +77,18 @@ Function Invoke-ExecModifyCalPerms { try { Write-LogMessage -headers $Request.Headers -API $APINAME-message "Processing target user: $TargetUser" -Sev 'Debug' - if ($Modification -eq 'Remove') { - try { - $CalPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Remove-MailboxFolderPermission' -cmdParams @{ - Identity = "$($userid):\$FolderName" - User = $TargetUser - Confirm = $false - } - $null = $results.Add("Removed $($TargetUser) from $($username) $FolderName permissions") - } - catch { - $null = $results.Add("No existing permissions to remove for $($TargetUser)") - } - } - else { - Write-LogMessage -headers $Request.Headers -API $APINAME-message "Setting permissions with AccessRights: $PermissionLevel" -Sev 'Debug' - - $cmdParams = @{ - Identity = "$($userid):\$FolderName" - User = $TargetUser - AccessRights = $PermissionLevel - Confirm = $false - } - - if ($CanViewPrivateItems) { - $cmdParams['SharingPermissionFlags'] = 'Delegate,CanViewPrivateItems' - } - - try { - # Try Add first - $CalPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Add-MailboxFolderPermission' -cmdParams $cmdParams - $null = $results.Add("Granted $($TargetUser) $($PermissionLevel) access to $($username) $FolderName$($CanViewPrivateItems ? ' with access to private items' : '')") - } - catch { - # If Add fails, try Set - $CalPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Set-MailboxFolderPermission' -cmdParams $cmdParams - $null = $results.Add("Updated $($TargetUser) $($PermissionLevel) access to $($username) $FolderName$($CanViewPrivateItems ? ' with access to private items' : '')") - } - } + $Result = Set-CIPPCalendarPermission -APIName $APIName ` + -Headers $Request.Headers ` + -RemoveAccess $(if ($Modification -eq 'Remove') { $TargetUser } else { $null }) ` + -TenantFilter $Tenantfilter ` + -UserID $userid ` + -folderName $FolderName ` + -UserToGetPermissions $TargetUser ` + -LoggingName $TargetUser ` + -Permissions $PermissionLevel ` + -CanViewPrivateItems $CanViewPrivateItems + + $null = $results.Add($Result) Write-LogMessage -headers $Request.Headers -API $APINAME-message "Successfully executed $($PermissionLevel) permission modification for $($TargetUser) on $($username)" -Sev 'Info' -tenant $TenantFilter } catch { From 5ccd2b712aca90f11a1f052efd9648ff3f910cab Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 10 Jun 2025 18:25:39 +0200 Subject: [PATCH 071/160] backtics to splat. --- .../Invoke-ExecModifyCalPerms.ps1 | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) 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 9d662bc4533d..30e2515432c3 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 @@ -12,14 +12,14 @@ Function Invoke-ExecModifyCalPerms { $APIName = $Request.Params.CIPPEndpoint Write-LogMessage -headers $Request.Headers -API $APINAME-message 'Accessed this API' -Sev 'Debug' - + $Username = $request.body.userID $Tenantfilter = $request.body.tenantfilter $Permissions = $request.body.permissions Write-LogMessage -headers $Request.Headers -API $APINAME-message "Processing request for user: $Username, tenant: $Tenantfilter" -Sev 'Debug' - if ($username -eq $null) { + if ($username -eq $null) { Write-LogMessage -headers $Request.Headers -API $APINAME-message 'Username is null' -Sev 'Error' $body = [pscustomobject]@{'Results' = @('Username is required') } Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ @@ -28,12 +28,11 @@ Function Invoke-ExecModifyCalPerms { }) return } - + try { $userid = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$($username)" -tenantid $Tenantfilter).id Write-LogMessage -headers $Request.Headers -API $APINAME-message "Retrieved user ID: $userid" -Sev 'Debug' - } - catch { + } catch { Write-LogMessage -headers $Request.Headers -API $APINAME-message "Failed to get user ID: $($_.Exception.Message)" -Sev 'Error' $body = [pscustomobject]@{'Results' = @("Failed to get user ID: $($_.Exception.Message)") } Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ @@ -50,8 +49,7 @@ Function Invoke-ExecModifyCalPerms { if ($Permissions -is [PSCustomObject]) { if ($Permissions.PSObject.Properties.Name -match '^\d+$') { $Permissions = $Permissions.PSObject.Properties.Value - } - else { + } else { $Permissions = @($Permissions) } } @@ -60,14 +58,14 @@ Function Invoke-ExecModifyCalPerms { foreach ($Permission in $Permissions) { Write-LogMessage -headers $Request.Headers -API $APINAME-message "Processing permission: $($Permission | ConvertTo-Json)" -Sev 'Debug' - + $PermissionLevel = $Permission.PermissionLevel.value ?? $Permission.PermissionLevel $Modification = $Permission.Modification $CanViewPrivateItems = $Permission.CanViewPrivateItems ?? $false $FolderName = $Permission.FolderName ?? 'Calendar' - + Write-LogMessage -headers $Request.Headers -API $APINAME-message "Permission Level: $PermissionLevel, Modification: $Modification, CanViewPrivateItems: $CanViewPrivateItems, FolderName: $FolderName" -Sev 'Debug' - + # Handle UserID as array or single value $TargetUsers = @($Permission.UserID | ForEach-Object { $_.value ?? $_ }) @@ -76,22 +74,24 @@ Function Invoke-ExecModifyCalPerms { foreach ($TargetUser in $TargetUsers) { try { Write-LogMessage -headers $Request.Headers -API $APINAME-message "Processing target user: $TargetUser" -Sev 'Debug' - - $Result = Set-CIPPCalendarPermission -APIName $APIName ` - -Headers $Request.Headers ` - -RemoveAccess $(if ($Modification -eq 'Remove') { $TargetUser } else { $null }) ` - -TenantFilter $Tenantfilter ` - -UserID $userid ` - -folderName $FolderName ` - -UserToGetPermissions $TargetUser ` - -LoggingName $TargetUser ` - -Permissions $PermissionLevel ` - -CanViewPrivateItems $CanViewPrivateItems + $Params = @{ + APIName = $APIName + Headers = $Request.Headers + RemoveAccess = if ($Modification -eq 'Remove') { $TargetUser } else { $null } + TenantFilter = $Tenantfilter + UserID = $userid + folderName = $FolderName + UserToGetPermissions = $TargetUser + LoggingName = $TargetUser + Permissions = $PermissionLevel + CanViewPrivateItems = $CanViewPrivateItems + } + + $Result = Set-CIPPCalendarPermission @Params $null = $results.Add($Result) Write-LogMessage -headers $Request.Headers -API $APINAME-message "Successfully executed $($PermissionLevel) permission modification for $($TargetUser) on $($username)" -Sev 'Info' -tenant $TenantFilter - } - catch { + } catch { $HasErrors = $true Write-LogMessage -headers $Request.Headers -API $APINAME-message "Could not execute $($PermissionLevel) permission modification for $($TargetUser) on $($username). Error: $($_.Exception.Message)" -Sev 'Error' -tenant $TenantFilter $null = $results.Add("Could not execute $($PermissionLevel) permission modification for $($TargetUser) on $($username). Error: $($_.Exception.Message)") @@ -112,4 +112,4 @@ Function Invoke-ExecModifyCalPerms { StatusCode = if ($HasErrors) { [HttpStatusCode]::InternalServerError } else { [HttpStatusCode]::OK } Body = $Body }) -} \ No newline at end of file +} From 022d8fdc72fc7310639d2a24f5b8d951a97384fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Tue, 10 Jun 2025 19:01:37 +0200 Subject: [PATCH 072/160] better logging and a report mode fix --- ...ndardDisableAdditionalStorageProviders.ps1 | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAdditionalStorageProviders.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAdditionalStorageProviders.ps1 index 2608c92914fc..74ca49628813 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAdditionalStorageProviders.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAdditionalStorageProviders.ps1 @@ -33,21 +33,21 @@ function Invoke-CIPPStandardDisableAdditionalStorageProviders { param($Tenant, $Settings) ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'DisableAdditionalStorageProviders' - $AdditionalStorageProvidersState = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-OwaMailboxPolicy' -cmdParams @{Identity = 'OwaMailboxPolicy-Default' } + $AdditionalStorageProvidersState = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-OwaMailboxPolicy' -cmdParams @{Identity = 'OwaMailboxPolicy-Default' } -Select 'Identity, AdditionalStorageProvidersAvailable' if ($Settings.remediate -eq $true) { try { if ($AdditionalStorageProvidersState.AdditionalStorageProvidersAvailable) { - New-ExoRequest -tenantid $Tenant -cmdlet 'Set-OwaMailboxPolicy' -cmdParams @{ Identity = $AdditionalStorageProvidersState.Identity; AdditionalStorageProvidersAvailable = $false } -useSystemMailbox $true - Write-LogMessage -API 'Standards' -tenant $tenant -message 'OWA additional storage providers have been disabled.' -sev Info + $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-OwaMailboxPolicy' -cmdParams @{ Identity = $AdditionalStorageProvidersState.Identity; AdditionalStorageProvidersAvailable = $false } -useSystemMailbox $true + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'OWA additional storage providers has been disabled.' -sev Info $AdditionalStorageProvidersState.AdditionalStorageProvidersAvailable = $false } else { - Write-LogMessage -API 'Standards' -tenant $tenant -message 'OWA additional storage providers are already disabled.' -sev Info + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'OWA additional storage providers are already disabled.' -sev Info } } catch { - $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to disable OWA additional storage providers. Error: $ErrorMessage" -sev Error + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to disable OWA additional storage providers. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage } } @@ -55,16 +55,16 @@ function Invoke-CIPPStandardDisableAdditionalStorageProviders { if ($Settings.alert -eq $true) { if ($AdditionalStorageProvidersState.AdditionalStorageProvidersAvailable) { $Object = $AdditionalStorageProvidersState | Select-Object -Property AdditionalStorageProvidersAvailable - Write-StandardsAlert -message 'OWA additional storage providers are enabled' -object $Object -tenant $tenant -standardName 'DisableAdditionalStorageProviders' -standardId $Settings.standardId - Write-LogMessage -API 'Standards' -tenant $tenant -message 'OWA additional storage providers are enabled' -sev Info + Write-StandardsAlert -message 'OWA additional storage providers are enabled' -object $Object -tenant $Tenant -standardName 'DisableAdditionalStorageProviders' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'OWA additional storage providers are enabled' -sev Info } else { - Write-LogMessage -API 'Standards' -tenant $tenant -message 'OWA additional storage providers are disabled' -sev Info + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'OWA additional storage providers are disabled' -sev Info } } if ($Settings.report -eq $true) { - $state = $AdditionalStorageProvidersState.AdditionalStorageProvidersEnabled ? $false : $true - Set-CIPPStandardsCompareField -FieldName 'standards.DisableAdditionalStorageProviders' -FieldValue $state -TenantFilter $Tenant - Add-CIPPBPAField -FieldName 'AdditionalStorageProvidersEnabled' -FieldValue $AdditionalStorageProvidersState.AdditionalStorageProvidersEnabled -StoreAs bool -Tenant $tenant + $State = $AdditionalStorageProvidersState.AdditionalStorageProvidersEnabled ? $false : $true + Set-CIPPStandardsCompareField -FieldName 'standards.DisableAdditionalStorageProviders' -FieldValue $State -TenantFilter $Tenant + Add-CIPPBPAField -FieldName 'AdditionalStorageProvidersEnabled' -FieldValue $State -StoreAs bool -Tenant $Tenant } } From 97c8c01b82e926ae56b5a9e4c24ef454de477967 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Tue, 10 Jun 2025 19:03:51 +0200 Subject: [PATCH 073/160] better logging and fix alert being wrong --- ...CIPPStandardPWcompanionAppAllowedState.ps1 | 43 +++++++++---------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPWcompanionAppAllowedState.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPWcompanionAppAllowedState.ps1 index 2322f74afe7f..b62bc33b3d1d 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPWcompanionAppAllowedState.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPWcompanionAppAllowedState.ps1 @@ -30,32 +30,31 @@ function Invoke-CIPPStandardPWcompanionAppAllowedState { param($Tenant, $Settings) - $authenticatorFeaturesState = (New-GraphGetRequest -tenantid $Tenant -Uri 'https://graph.microsoft.com/beta/policies/authenticationMethodsPolicy/authenticationMethodConfigurations/microsoftAuthenticator') - $authState = if ($authenticatorFeaturesState.featureSettings.companionAppAllowedState.state -eq 'enabled') { $true } else { $false } - - + $AuthenticatorFeaturesState = (New-GraphGetRequest -tenantid $Tenant -Uri 'https://graph.microsoft.com/beta/policies/authenticationMethodsPolicy/authenticationMethodConfigurations/microsoftAuthenticator') # Get state value using null-coalescing operator - $state = $Settings.state.value ? $Settings.state.value : $settings.state - $authState = if ($authenticatorFeaturesState.featureSettings.companionAppAllowedState.state -eq $state) { $true } else { $false } + $CurrentState = $AuthenticatorFeaturesState.featureSettings.companionAppAllowedState.state + $WantedState = $Settings.state.value ? $Settings.state.value : $settings.state + $AuthStateCorrect = if ($CurrentState -eq $WantedState) { $true } else { $false } # Input validation - if (([string]::IsNullOrWhiteSpace($state) -or $state -eq 'Select a value') -and ($Settings.remediate -eq $true -or $Settings.alert -eq $true)) { + if (([string]::IsNullOrWhiteSpace($WantedState) -or $WantedState -eq 'Select a value') -and ($Settings.remediate -eq $true -or $Settings.alert -eq $true)) { Write-LogMessage -API 'Standards' -tenant $Tenant -message 'PWcompanionAppAllowedState: Invalid state parameter set' -sev Error Return } If ($Settings.remediate -eq $true) { + Write-Host "Remediating PWcompanionAppAllowedState for tenant $Tenant to $WantedState" - if ($authenticatorFeaturesState.featureSettings.companionAppAllowedState.state -eq $state) { - Write-LogMessage -API 'Standards' -tenant $Tenant -message "companionAppAllowedState is already set to the desired state of $state." -sev Info + if ($AuthStateCorrect -eq $true) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "companionAppAllowedState is already set to the desired state of $WantedState." -sev Info } else { try { # Remove number matching from featureSettings because this is now Microsoft enforced and shipping it returns an error - $authenticatorFeaturesState.featureSettings.PSObject.Properties.Remove('numberMatchingRequiredState') + $AuthenticatorFeaturesState.featureSettings.PSObject.Properties.Remove('numberMatchingRequiredState') # Define feature body $featureBody = @{ - state = $state + state = $WantedState includeTarget = [PSCustomObject]@{ targetType = 'group' id = 'all_users' @@ -65,33 +64,33 @@ function Invoke-CIPPStandardPWcompanionAppAllowedState { id = '00000000-0000-0000-0000-000000000000' } } - $authenticatorFeaturesState.featureSettings.companionAppAllowedState = $featureBody - $body = ConvertTo-Json -Depth 3 -Compress -InputObject $authenticatorFeaturesState + $AuthenticatorFeaturesState.featureSettings.companionAppAllowedState = $featureBody + $body = ConvertTo-Json -Depth 3 -Compress -InputObject $AuthenticatorFeaturesState $null = (New-GraphPostRequest -tenantid $Tenant -Uri 'https://graph.microsoft.com/beta/policies/authenticationMethodsPolicy/authenticationMethodConfigurations/microsoftAuthenticator' -Type patch -Body $body -ContentType 'application/json') - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Set companionAppAllowedState to $state." -sev Info + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Set companionAppAllowedState to $WantedState." -sev Info } catch { $ErrorMessage = Get-CippExceptionMessage -Exception $_ - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to set companionAppAllowedState to $state. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to set companionAppAllowedState to $WantedState. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage } } } if ($Settings.alert -eq $true) { - if ($authState) { - Write-LogMessage -API 'Standards' -tenant $Tenant -message 'companionAppAllowedState is enabled.' -sev Info + if ($AuthStateCorrect -eq $true) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "companionAppAllowedState is set to $WantedState." -sev Info } else { - Write-StandardsAlert -message 'companionAppAllowedState is not enabled' -object $authenticatorFeaturesState -tenant $Tenant -standardName 'PWcompanionAppAllowedState' -standardId $Settings.standardId - Write-LogMessage -API 'Standards' -tenant $Tenant -message 'companionAppAllowedState is not enabled.' -sev Info + Write-StandardsAlert -message "companionAppAllowedState is not set to $WantedState. Current state is $CurrentState." -object $AuthenticatorFeaturesState -tenant $Tenant -standardName 'PWcompanionAppAllowedState' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $Tenant -message "companionAppAllowedState is not set to $WantedState. Current state is $CurrentState." -sev Info } } if ($Settings.report -eq $true) { - Add-CIPPBPAField -FieldName 'companionAppAllowedState' -FieldValue $authState -StoreAs bool -Tenant $Tenant - if ($authState) { + Add-CIPPBPAField -FieldName 'companionAppAllowedState' -FieldValue $AuthStateCorrect -StoreAs bool -Tenant $Tenant + if ($AuthStateCorrect -eq $true) { $FieldValue = $true } else { - $FieldValue = $authenticatorFeaturesState.featureSettings.companionAppAllowedState + $FieldValue = $AuthenticatorFeaturesState.featureSettings.companionAppAllowedState } Set-CIPPStandardsCompareField -FieldName 'standards.PWcompanionAppAllowedState' -FieldValue $FieldValue -Tenant $Tenant } From 4e8624a87bce49d7a27058fbae47d0911ab811a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Tue, 10 Jun 2025 22:28:22 +0200 Subject: [PATCH 074/160] Fix casing of GetEnumerator method and standardize tenant variable usage in logging --- .../GraphHelper/New-GraphPOSTRequest.ps1 | 2 +- ...voke-CIPPStandardDisableTenantCreation.ps1 | 25 +++++++++---------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1 index 73fa5b11845c..4a86eebf73c0 100644 --- a/Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1 @@ -7,7 +7,7 @@ function New-GraphPOSTRequest ($uri, $tenantid, $body, $type, $scope, $AsApp, $N if ($NoAuthCheck -or (Get-AuthorisedRequest -Uri $uri -TenantID $tenantid)) { $headers = Get-GraphToken -tenantid $tenantid -scope $scope -AsApp $asapp -SkipCache $skipTokenCache if ($AddedHeaders) { - foreach ($header in $AddedHeaders.getenumerator()) { + foreach ($header in $AddedHeaders.GetEnumerator()) { $headers.Add($header.Key, $header.Value) } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTenantCreation.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTenantCreation.ps1 index 59487435b343..fcd876447843 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTenantCreation.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTenantCreation.ps1 @@ -37,37 +37,36 @@ function Invoke-CIPPStandardDisableTenantCreation { $StateIsCorrect = ($CurrentState.defaultUserRolePermissions.allowedToCreateTenants -eq $false) If ($Settings.remediate -eq $true) { + Write-Host "Time to remediate DisableTenantCreation standard for tenant $Tenant" if ($StateIsCorrect -eq $true) { - Write-LogMessage -API 'Standards' -tenant $tenant -message 'Users are already disabled from creating tenants.' -sev Info + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Users are already disabled from creating tenants.' -sev Info } else { try { $GraphRequest = @{ - tenantid = $tenant - uri = 'https://graph.microsoft.com/beta/policies/authorizationPolicy/authorizationPolicy' - AsApp = $false - Type = 'PATCH' - ContentType = 'application/json' - Body = '{"defaultUserRolePermissions":{"allowedToCreateTenants":false}}' + tenantid = $Tenant + uri = 'https://graph.microsoft.com/beta/policies/authorizationPolicy/authorizationPolicy' + Type = 'PATCH' + Body = '{"defaultUserRolePermissions":{"allowedToCreateTenants":false}}' } New-GraphPostRequest @GraphRequest - Write-LogMessage -API 'Standards' -tenant $tenant -message 'Disabled users from creating tenants.' -sev Info + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Disabled users from creating tenants.' -sev Info } catch { - Write-LogMessage -API 'Standards' -tenant $tenant -message 'Failed to disable users from creating tenants' -sev 'Error' -LogData $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Failed to disable users from creating tenants' -sev 'Error' -LogData $_ } } } if ($Settings.alert -eq $true) { if ($StateIsCorrect -eq $true) { - Write-LogMessage -API 'Standards' -tenant $tenant -message 'Users are not allowed to create tenants.' -sev Info + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Users are not allowed to create tenants.' -sev Info } else { - Write-StandardsAlert -message 'Users are allowed to create tenants' -object $CurrentState -tenant $tenant -standardName 'DisableTenantCreation' -standardId $Settings.standardId - Write-LogMessage -API 'Standards' -tenant $tenant -message 'Users are allowed to create tenants.' -sev Info + Write-StandardsAlert -message 'Users are allowed to create tenants' -object $CurrentState -tenant $Tenant -standardName 'DisableTenantCreation' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Users are allowed to create tenants.' -sev Info } } if ($Settings.report -eq $true) { Set-CIPPStandardsCompareField -FieldName 'standards.DisableTenantCreation' -FieldValue $StateIsCorrect -TenantFilter $Tenant - Add-CIPPBPAField -FieldName 'DisableTenantCreation' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant + Add-CIPPBPAField -FieldName 'DisableTenantCreation' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant } } From d26ce7db49aed808ed2aa03441edfb82e118035c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Tue, 10 Jun 2025 22:30:49 +0200 Subject: [PATCH 075/160] more logging --- .../Standards/Invoke-CIPPStandardDisableTenantCreation.ps1 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTenantCreation.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTenantCreation.ps1 index fcd876447843..1980a05dcbb8 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTenantCreation.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTenantCreation.ps1 @@ -49,9 +49,10 @@ function Invoke-CIPPStandardDisableTenantCreation { Body = '{"defaultUserRolePermissions":{"allowedToCreateTenants":false}}' } New-GraphPostRequest @GraphRequest - Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Disabled users from creating tenants.' -sev Info + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Successfully disabled users from creating tenants.' -sev Info } catch { - Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Failed to disable users from creating tenants' -sev 'Error' -LogData $_ + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to disable users from creating tenants. Error: $($ErrorMessage.NormalizedError)" -sev 'Error' -LogData $ErrorMessage } } } From 93f143849480d9fc1f2d6345a19bdbf6a36e39ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Tue, 10 Jun 2025 22:33:22 +0200 Subject: [PATCH 076/160] More casing --- .../Standards/Invoke-CIPPStandardDisableTenantCreation.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTenantCreation.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTenantCreation.ps1 index 1980a05dcbb8..dcdab63d0073 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTenantCreation.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTenantCreation.ps1 @@ -48,7 +48,7 @@ function Invoke-CIPPStandardDisableTenantCreation { Type = 'PATCH' Body = '{"defaultUserRolePermissions":{"allowedToCreateTenants":false}}' } - New-GraphPostRequest @GraphRequest + New-GraphPOSTRequest @GraphRequest Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Successfully disabled users from creating tenants.' -sev Info } catch { $ErrorMessage = Get-CippException -Exception $_ From 2a375f3f1c04f6b8c7881787843268453b102a0b Mon Sep 17 00:00:00 2001 From: Jr7468 Date: Tue, 10 Jun 2025 23:31:26 +0100 Subject: [PATCH 077/160] Fixed permission node. --- .../Invoke-ExecModifyCalPerms.ps1 | 81 ++++++++++++------- 1 file changed, 53 insertions(+), 28 deletions(-) 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 30e2515432c3..2431861d5c3e 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 @@ -5,21 +5,21 @@ Function Invoke-ExecModifyCalPerms { .FUNCTIONALITY Entrypoint .ROLE - Exchange.Calendar.ReadWrite + Exchange.Mailbox.ReadWrite #> [CmdletBinding()] param($Request, $TriggerMetadata) $APIName = $Request.Params.CIPPEndpoint Write-LogMessage -headers $Request.Headers -API $APINAME-message 'Accessed this API' -Sev 'Debug' - + $Username = $request.body.userID $Tenantfilter = $request.body.tenantfilter $Permissions = $request.body.permissions Write-LogMessage -headers $Request.Headers -API $APINAME-message "Processing request for user: $Username, tenant: $Tenantfilter" -Sev 'Debug' - if ($username -eq $null) { + if ($username -eq $null) { Write-LogMessage -headers $Request.Headers -API $APINAME-message 'Username is null' -Sev 'Error' $body = [pscustomobject]@{'Results' = @('Username is required') } Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ @@ -28,11 +28,12 @@ Function Invoke-ExecModifyCalPerms { }) return } - + try { $userid = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$($username)" -tenantid $Tenantfilter).id Write-LogMessage -headers $Request.Headers -API $APINAME-message "Retrieved user ID: $userid" -Sev 'Debug' - } catch { + } + catch { Write-LogMessage -headers $Request.Headers -API $APINAME-message "Failed to get user ID: $($_.Exception.Message)" -Sev 'Error' $body = [pscustomobject]@{'Results' = @("Failed to get user ID: $($_.Exception.Message)") } Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ @@ -49,7 +50,8 @@ Function Invoke-ExecModifyCalPerms { if ($Permissions -is [PSCustomObject]) { if ($Permissions.PSObject.Properties.Name -match '^\d+$') { $Permissions = $Permissions.PSObject.Properties.Value - } else { + } + else { $Permissions = @($Permissions) } } @@ -58,14 +60,13 @@ Function Invoke-ExecModifyCalPerms { foreach ($Permission in $Permissions) { Write-LogMessage -headers $Request.Headers -API $APINAME-message "Processing permission: $($Permission | ConvertTo-Json)" -Sev 'Debug' - + $PermissionLevel = $Permission.PermissionLevel.value ?? $Permission.PermissionLevel $Modification = $Permission.Modification $CanViewPrivateItems = $Permission.CanViewPrivateItems ?? $false - $FolderName = $Permission.FolderName ?? 'Calendar' - - Write-LogMessage -headers $Request.Headers -API $APINAME-message "Permission Level: $PermissionLevel, Modification: $Modification, CanViewPrivateItems: $CanViewPrivateItems, FolderName: $FolderName" -Sev 'Debug' - + + Write-LogMessage -headers $Request.Headers -API $APINAME-message "Permission Level: $PermissionLevel, Modification: $Modification, CanViewPrivateItems: $CanViewPrivateItems" -Sev 'Debug' + # Handle UserID as array or single value $TargetUsers = @($Permission.UserID | ForEach-Object { $_.value ?? $_ }) @@ -74,24 +75,48 @@ Function Invoke-ExecModifyCalPerms { foreach ($TargetUser in $TargetUsers) { try { Write-LogMessage -headers $Request.Headers -API $APINAME-message "Processing target user: $TargetUser" -Sev 'Debug' - $Params = @{ - APIName = $APIName - Headers = $Request.Headers - RemoveAccess = if ($Modification -eq 'Remove') { $TargetUser } else { $null } - TenantFilter = $Tenantfilter - UserID = $userid - folderName = $FolderName - UserToGetPermissions = $TargetUser - LoggingName = $TargetUser - Permissions = $PermissionLevel - CanViewPrivateItems = $CanViewPrivateItems + + if ($Modification -eq 'Remove') { + try { + $CalPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Remove-MailboxFolderPermission' -cmdParams @{ + Identity = "$($userid):\Calendar" + User = $TargetUser + Confirm = $false + } + $null = $results.Add("Removed $($TargetUser) from $($username) Calendar permissions") + } + catch { + $null = $results.Add("No existing permissions to remove for $($TargetUser)") + } + } + else { + Write-LogMessage -headers $Request.Headers -API $APINAME-message "Setting permissions with AccessRights: $PermissionLevel" -Sev 'Debug' + + $cmdParams = @{ + Identity = "$($userid):\Calendar" + User = $TargetUser + AccessRights = $PermissionLevel + Confirm = $false + } + + if ($CanViewPrivateItems) { + $cmdParams['SharingPermissionFlags'] = 'Delegate,CanViewPrivateItems' + } + + try { + # Try Add first + $CalPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Add-MailboxFolderPermission' -cmdParams $cmdParams + $null = $results.Add("Granted $($TargetUser) $($PermissionLevel) access to $($username) Calendar$($CanViewPrivateItems ? ' with access to private items' : '')") + } + catch { + # If Add fails, try Set + $CalPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Set-MailboxFolderPermission' -cmdParams $cmdParams + $null = $results.Add("Updated $($TargetUser) $($PermissionLevel) access to $($username) Calendar$($CanViewPrivateItems ? ' with access to private items' : '')") + } } - - $Result = Set-CIPPCalendarPermission @Params - - $null = $results.Add($Result) Write-LogMessage -headers $Request.Headers -API $APINAME-message "Successfully executed $($PermissionLevel) permission modification for $($TargetUser) on $($username)" -Sev 'Info' -tenant $TenantFilter - } catch { + } + catch { $HasErrors = $true Write-LogMessage -headers $Request.Headers -API $APINAME-message "Could not execute $($PermissionLevel) permission modification for $($TargetUser) on $($username). Error: $($_.Exception.Message)" -Sev 'Error' -tenant $TenantFilter $null = $results.Add("Could not execute $($PermissionLevel) permission modification for $($TargetUser) on $($username). Error: $($_.Exception.Message)") @@ -112,4 +137,4 @@ Function Invoke-ExecModifyCalPerms { StatusCode = if ($HasErrors) { [HttpStatusCode]::InternalServerError } else { [HttpStatusCode]::OK } Body = $Body }) -} +} \ No newline at end of file From 0480eb80f6cfd5438fe1d3448da09304020e9b61 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 11 Jun 2025 10:26:45 -0400 Subject: [PATCH 078/160] Fix BPA import --- .../Orchestrator Functions/Start-BPAOrchestrator.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-BPAOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-BPAOrchestrator.ps1 index 15b91b83d47d..71e79f07a75a 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-BPAOrchestrator.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-BPAOrchestrator.ps1 @@ -46,7 +46,7 @@ function Start-BPAOrchestrator { PartitionKey = 'BPATemplate' GUID = "$($_.name)" } - Add-CIPPAzDataTableEntity @Table -Entity $Entity -Force + Add-CIPPAzDataTableEntity @BPATemplateTable -Entity $Entity -Force } $TemplateRows = Get-CIPPAzDataTableEntity @BPATemplateTable -Filter $Filter } From 615737a9f26ded19df855956d8e47267f0a61a8f Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 11 Jun 2025 16:51:11 -0400 Subject: [PATCH 079/160] fix exclude from standards in onboarding --- .../Activity Triggers/Push-ExecOnboardTenantQueue.ps1 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecOnboardTenantQueue.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecOnboardTenantQueue.ps1 index 210dbda78e27..5473c27351ca 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecOnboardTenantQueue.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecOnboardTenantQueue.ps1 @@ -366,6 +366,9 @@ function Push-ExecOnboardTenantQueue { foreach ($AllTenantsTemplate in $ExistingTemplates) { $object = $AllTenantesTemplate.JSON | ConvertFrom-Json $NewExcludedTenants = [system.collections.generic.list[object]]::new() + if (!$object.excludedTenants) { + $object | Add-Member -MemberType NoteProperty -Name 'excludedTenants' -Value @() -Force + } foreach ($Tenant in $object.excludedTenants) { $NewExcludedTenants.Add($Tenant) } From 49741ff0fb57fcf8798926fd60671adea617214c Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 12 Jun 2025 13:26:01 -0400 Subject: [PATCH 080/160] prevent missing refresh_token in claims --- .../HTTP Functions/CIPP/Setup/Invoke-ExecAddTenant.ps1 | 6 +++++- Modules/CIPPCore/Public/GraphHelper/Get-GraphToken.ps1 | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecAddTenant.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecAddTenant.ps1 index 0bd359ccb156..28e2e79e37da 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecAddTenant.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecAddTenant.ps1 @@ -22,7 +22,11 @@ function Invoke-ExecAddTenant { # Check if tenant already exists $ExistingTenant = Get-CIPPAzDataTableEntity @TenantsTable -Filter "PartitionKey eq 'Tenants' and RowKey eq '$tenantId'" - if ($ExistingTenant) { + if ($tenantId -eq $env:TenantID) { + # If the tenant is the partner tenant, return an error because you cannot add the partner tenant as direct tenant + $Results = @{'message' = 'You cannot add the partner tenant as a direct tenant.'; 'severity' = 'error'; 'state' = 'error' } + Write-LogMessage -API 'Add-Tenant' -message "Attempted to add partner tenant $tenantId as direct tenant." -Sev 'Error' + } elseif ($ExistingTenant) { # Update existing tenant $ExistingTenant.delegatedPrivilegeStatus = 'directTenant' Add-CIPPAzDataTableEntity @TenantsTable -Entity $ExistingTenant -Force | Out-Null diff --git a/Modules/CIPPCore/Public/GraphHelper/Get-GraphToken.ps1 b/Modules/CIPPCore/Public/GraphHelper/Get-GraphToken.ps1 index ac1ad36e954b..c89d8fd2dd75 100644 --- a/Modules/CIPPCore/Public/GraphHelper/Get-GraphToken.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/Get-GraphToken.ps1 @@ -21,7 +21,7 @@ function Get-GraphToken($tenantid, $scope, $AsApp, $AppID, $AppSecret, $refreshT $TenantsTable = Get-CippTable -tablename 'Tenants' $Filter = "PartitionKey eq 'Tenants' and delegatedPrivilegeStatus eq 'directTenant'" $ClientType = Get-CIPPAzDataTableEntity @TenantsTable -Filter $Filter | Where-Object { $_.customerId -eq $tenantid -or $_.defaultDomainName -eq $tenantid } - if ($clientType.delegatedPrivilegeStatus -eq 'directTenant') { + if ($tenantid -ne $env:TenantID -and $clientType.delegatedPrivilegeStatus -eq 'directTenant') { Write-Host "Using direct tenant refresh token for $($clientType.customerId)" $ClientRefreshToken = Get-Item -Path "env:\$($clientType.customerId)" -ErrorAction SilentlyContinue $refreshToken = $ClientRefreshToken.Value From 888cce4227ad217f64040782d5d51299258140ab Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 12 Jun 2025 13:44:42 -0400 Subject: [PATCH 081/160] Update Invoke-ExecAddTenant.ps1 --- .../HTTP Functions/CIPP/Setup/Invoke-ExecAddTenant.ps1 | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecAddTenant.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecAddTenant.ps1 index 28e2e79e37da..be1c4131ea69 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecAddTenant.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecAddTenant.ps1 @@ -24,8 +24,7 @@ function Invoke-ExecAddTenant { if ($tenantId -eq $env:TenantID) { # If the tenant is the partner tenant, return an error because you cannot add the partner tenant as direct tenant - $Results = @{'message' = 'You cannot add the partner tenant as a direct tenant.'; 'severity' = 'error'; 'state' = 'error' } - Write-LogMessage -API 'Add-Tenant' -message "Attempted to add partner tenant $tenantId as direct tenant." -Sev 'Error' + $Results = @{'message' = 'You cannot add the partner tenant as a direct tenant. Please connect the tenant using the "Connect to Partner Tenant" option. '; 'severity' = 'error'; } } elseif ($ExistingTenant) { # Update existing tenant $ExistingTenant.delegatedPrivilegeStatus = 'directTenant' From ec6c2c6ffd21742a8539c0b44f5a9c46b58e1d73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Thu, 12 Jun 2025 19:56:58 +0200 Subject: [PATCH 082/160] smol brain mistake fixed --- .../Tenant/Administration/Tenant/Invoke-ListTenantDetails.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenantDetails.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenantDetails.ps1 index 8f3bbccc0063..95cf208f7224 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenantDetails.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenantDetails.ps1 @@ -28,6 +28,7 @@ Function Invoke-ListTenantDetails { $Groups = (Get-TenantGroups -TenantFilter $TenantFilter) ?? @() $org | Add-Member -MemberType NoteProperty -Name 'Groups' -Value @($Groups) + $StatusCode = [HttpStatusCode]::OK } catch { $ErrorMessage = Get-CippException -Exception $_ From a84d74283861dc0fd6f8a1df69c50481db5bd691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Thu, 12 Jun 2025 20:05:04 +0200 Subject: [PATCH 083/160] feat: add support for renaming named locations --- .../Conditional/Invoke-ExecNamedLocation.ps1 | 20 ++++++------- .../CIPPCore/Public/Set-CIPPNamedLocation.ps1 | 30 ++++++++++++------- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ExecNamedLocation.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ExecNamedLocation.ps1 index 38653783e2e6..e98898f6a858 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ExecNamedLocation.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ExecNamedLocation.ps1 @@ -15,26 +15,26 @@ function Invoke-ExecNamedLocation { Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + # Interact with query parameters or the body of the request. $TenantFilter = $Request.Body.tenantFilter ?? $Request.Query.tenantFilter - $NamedLocationId = $Request.Body.NamedLocationId ?? $Request.Query.NamedLocationId - $change = $Request.Body.change ?? $Request.Query.change - $content = $Request.Body.input ?? $Request.Query.input + $NamedLocationId = $Request.Body.namedLocationId ?? $Request.Query.namedLocationId + $Change = $Request.Body.change ?? $Request.Query.change + $Content = $Request.Body.input ?? $Request.Query.input try { - $results = Set-CIPPNamedLocation -NamedLocationId $NamedLocationId -TenantFilter $TenantFilter -change $change -content $content -Headers $Request.Headers + $results = Set-CIPPNamedLocation -NamedLocationId $NamedLocationId -TenantFilter $TenantFilter -Change $Change -Content $Content -Headers $Headers + $StatusCode = [HttpStatusCode]::OK } catch { $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -headers $Request.Headers -API $APIName -message "Failed to edit named location: $($ErrorMessage.NormalizedError)" -Sev 'Error' -tenant $TenantFilter -LogData $ErrorMessage + Write-LogMessage -headers $Headers -API $APIName -message "Failed to edit named location: $($ErrorMessage.NormalizedError)" -Sev 'Error' -tenant $TenantFilter -LogData $ErrorMessage $results = "Failed to edit named location. Error: $($ErrorMessage.NormalizedError)" + $StatusCode = [HttpStatusCode]::InternalServerError } - - $body = [pscustomobject]@{'Results' = @($results) } - # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ - StatusCode = [HttpStatusCode]::OK - Body = $body + StatusCode = $StatusCode + Body = @{'Results' = @($results) } }) } diff --git a/Modules/CIPPCore/Public/Set-CIPPNamedLocation.ps1 b/Modules/CIPPCore/Public/Set-CIPPNamedLocation.ps1 index 3f23365c31be..7e369cee4180 100644 --- a/Modules/CIPPCore/Public/Set-CIPPNamedLocation.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPNamedLocation.ps1 @@ -3,10 +3,10 @@ function Set-CIPPNamedLocation { param( $NamedLocationId, $TenantFilter, - #$change should be one of 'addip','addlocation','removeip','removelocation' - [ValidateSet('addip', 'addlocation', 'removeip', 'removelocation')] - $change, - $content, + #$Change should be one of 'addIp','addLocation','removeIp','removeLocation','rename' + [ValidateSet('addIp', 'addLocation', 'removeIp', 'removeLocation', 'rename')] + $Change, + $Content, $APIName = 'Set Named Location', $Headers ) @@ -14,23 +14,33 @@ function Set-CIPPNamedLocation { try { $NamedLocations = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations/$NamedLocationId" -Tenantid $tenantfilter switch ($change) { - 'addip' { + 'addIp' { $NamedLocations.ipRanges = @($NamedLocations.ipRanges + @{ cidrAddress = $content; '@odata.type' = '#microsoft.graph.iPv4CidrRange' }) } - 'addlocation' { + 'addLocation' { $NamedLocations.countriesAndRegions = $NamedLocations.countriesAndRegions + $content } - 'removeip' { + 'removeIp' { $NamedLocations.ipRanges = @($NamedLocations.ipRanges | Where-Object -Property cidrAddress -NE $content) } - 'removelocation' { + 'removeLocation' { $NamedLocations.countriesAndRegions = @($NamedLocations.countriesAndRegions | Where-Object { $_ -NE $content }) } + 'rename' { + $NamedLocations.displayName = $content + } } if ($PSCmdlet.ShouldProcess($GroupName, "Assigning Application $ApplicationId")) { - #Remove unneeded propertie + #Remove unneeded properties if ($change -like '*location*') { $NamedLocations = $NamedLocations | Select-Object '@odata.type', 'displayName', 'countriesAndRegions', 'includeUnknownCountriesAndRegions' + } elseif ($change -eq 'rename') { + # For rename, only include the basic properties needed + if ($NamedLocations.'@odata.type' -eq '#microsoft.graph.countryNamedLocation') { + $NamedLocations = $NamedLocations | Select-Object '@odata.type', 'displayName', 'countriesAndRegions', 'includeUnknownCountriesAndRegions' + } else { + $NamedLocations = $NamedLocations | Select-Object '@odata.type', 'displayName', 'ipRanges', 'isTrusted' + } } else { $NamedLocations = $NamedLocations | Select-Object '@odata.type', 'displayName', 'ipRanges', 'isTrusted' } @@ -42,6 +52,6 @@ function Set-CIPPNamedLocation { } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -headers $Headers -API $APIName -message "Failed to edit named location: $($ErrorMessage.NormalizedError)" -Sev 'Error' -tenant $TenantFilter -LogData $ErrorMessage - return "Failed to edit named location. Error: $($ErrorMessage.NormalizedError)" + throw "Failed to edit named location. Error: $($ErrorMessage.NormalizedError)" } } From 0dd57a4c47c4c723f20c6b2a8861fad40fa6a0bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Thu, 12 Jun 2025 20:39:42 +0200 Subject: [PATCH 084/160] fix: standardize variable casing and improve error handling in Set-CIPPNamedLocation function --- .../CIPPCore/Public/Set-CIPPNamedLocation.ps1 | 38 +++++++++---------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/Modules/CIPPCore/Public/Set-CIPPNamedLocation.ps1 b/Modules/CIPPCore/Public/Set-CIPPNamedLocation.ps1 index 7e369cee4180..889c980851f3 100644 --- a/Modules/CIPPCore/Public/Set-CIPPNamedLocation.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPNamedLocation.ps1 @@ -12,46 +12,42 @@ function Set-CIPPNamedLocation { ) try { - $NamedLocations = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations/$NamedLocationId" -Tenantid $tenantfilter - switch ($change) { + $NamedLocations = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations/$NamedLocationId" -Tenantid $TenantFilter + switch ($Change) { 'addIp' { - $NamedLocations.ipRanges = @($NamedLocations.ipRanges + @{ cidrAddress = $content; '@odata.type' = '#microsoft.graph.iPv4CidrRange' }) + $NamedLocations.ipRanges = @($NamedLocations.ipRanges + @{ cidrAddress = $Content; '@odata.type' = '#microsoft.graph.iPv4CidrRange' }) } 'addLocation' { - $NamedLocations.countriesAndRegions = $NamedLocations.countriesAndRegions + $content + $NamedLocations.countriesAndRegions = $NamedLocations.countriesAndRegions + $Content } 'removeIp' { - $NamedLocations.ipRanges = @($NamedLocations.ipRanges | Where-Object -Property cidrAddress -NE $content) + $NamedLocations.ipRanges = @($NamedLocations.ipRanges | Where-Object -Property cidrAddress -NE $Content) } 'removeLocation' { - $NamedLocations.countriesAndRegions = @($NamedLocations.countriesAndRegions | Where-Object { $_ -NE $content }) + $NamedLocations.countriesAndRegions = @($NamedLocations.countriesAndRegions | Where-Object { $_ -NE $Content }) } 'rename' { - $NamedLocations.displayName = $content + $NamedLocations.displayName = $Content } } - if ($PSCmdlet.ShouldProcess($GroupName, "Assigning Application $ApplicationId")) { + if ($PSCmdlet.ShouldProcess($NamedLocations.displayName, "Editing named location: $($NamedLocations.displayName). Change: $Change with content $($Content)")) { #Remove unneeded properties - if ($change -like '*location*') { + if ($NamedLocations.'@odata.type' -eq '#microsoft.graph.countryNamedLocation') { $NamedLocations = $NamedLocations | Select-Object '@odata.type', 'displayName', 'countriesAndRegions', 'includeUnknownCountriesAndRegions' - } elseif ($change -eq 'rename') { - # For rename, only include the basic properties needed - if ($NamedLocations.'@odata.type' -eq '#microsoft.graph.countryNamedLocation') { - $NamedLocations = $NamedLocations | Select-Object '@odata.type', 'displayName', 'countriesAndRegions', 'includeUnknownCountriesAndRegions' - } else { - $NamedLocations = $NamedLocations | Select-Object '@odata.type', 'displayName', 'ipRanges', 'isTrusted' - } } else { $NamedLocations = $NamedLocations | Select-Object '@odata.type', 'displayName', 'ipRanges', 'isTrusted' } - $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations/$NamedLocationId" -tenantid $TenantFilter -type PATCH -body $($NamedLocations | ConvertTo-Json -Compress -Depth 10) - Write-LogMessage -headers $Headers -API $APIName -message "Edited named location. Change: $change with content $($content)" -Sev 'Info' -tenant $TenantFilter + $JsonBody = ConvertTo-Json -InputObject $NamedLocations -Compress -Depth 10 + $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations/$NamedLocationId" -tenantid $TenantFilter -type PATCH -body $JsonBody + $Result = "Edited named location: $($NamedLocations.displayName). Change: $Change with content $($Content)" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev 'Info' } - return "Edited named location. Change: $change with content $($content)" + return $Result } catch { $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -headers $Headers -API $APIName -message "Failed to edit named location: $($ErrorMessage.NormalizedError)" -Sev 'Error' -tenant $TenantFilter -LogData $ErrorMessage - throw "Failed to edit named location. Error: $($ErrorMessage.NormalizedError)" + $Result = "Failed to edit named location: $($NamedLocations.displayName). Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -tenant $TenantFilter -API $APIName -message $Result -Sev 'Error' -LogData $ErrorMessage + throw $Result } } From cc7acff637145354280544aae07003ba6ab61843 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Thu, 12 Jun 2025 21:03:01 +0200 Subject: [PATCH 085/160] feat: extend Set-CIPPNamedLocation function to support 'setTrusted' and 'setUntrusted' changes --- Modules/CIPPCore/Public/Set-CIPPNamedLocation.ps1 | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Modules/CIPPCore/Public/Set-CIPPNamedLocation.ps1 b/Modules/CIPPCore/Public/Set-CIPPNamedLocation.ps1 index 889c980851f3..a1e616579478 100644 --- a/Modules/CIPPCore/Public/Set-CIPPNamedLocation.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPNamedLocation.ps1 @@ -3,8 +3,8 @@ function Set-CIPPNamedLocation { param( $NamedLocationId, $TenantFilter, - #$Change should be one of 'addIp','addLocation','removeIp','removeLocation','rename' - [ValidateSet('addIp', 'addLocation', 'removeIp', 'removeLocation', 'rename')] + #$Change should be one of 'addIp','addLocation','removeIp','removeLocation','rename','setTrusted','setUntrusted' + [ValidateSet('addIp', 'addLocation', 'removeIp', 'removeLocation', 'rename', 'setTrusted', 'setUntrusted')] $Change, $Content, $APIName = 'Set Named Location', @@ -29,12 +29,18 @@ function Set-CIPPNamedLocation { 'rename' { $NamedLocations.displayName = $Content } + 'setTrusted' { + $NamedLocations.isTrusted = $true + } + 'setUntrusted' { + $NamedLocations.isTrusted = $false + } } if ($PSCmdlet.ShouldProcess($NamedLocations.displayName, "Editing named location: $($NamedLocations.displayName). Change: $Change with content $($Content)")) { #Remove unneeded properties if ($NamedLocations.'@odata.type' -eq '#microsoft.graph.countryNamedLocation') { $NamedLocations = $NamedLocations | Select-Object '@odata.type', 'displayName', 'countriesAndRegions', 'includeUnknownCountriesAndRegions' - } else { + } elseif ($NamedLocations.'@odata.type' -eq '#microsoft.graph.ipNamedLocation') { $NamedLocations = $NamedLocations | Select-Object '@odata.type', 'displayName', 'ipRanges', 'isTrusted' } From cd07ff7be4771d2c95848a8e0f66dcd43b2e639d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Thu, 12 Jun 2025 21:56:32 +0200 Subject: [PATCH 086/160] feat: enhance Set-CIPPNamedLocation function to support 'delete' action and improve action descriptions --- .../CIPPCore/Public/Set-CIPPNamedLocation.ps1 | 40 ++++++++++++++----- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/Modules/CIPPCore/Public/Set-CIPPNamedLocation.ps1 b/Modules/CIPPCore/Public/Set-CIPPNamedLocation.ps1 index a1e616579478..888622741f86 100644 --- a/Modules/CIPPCore/Public/Set-CIPPNamedLocation.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPNamedLocation.ps1 @@ -3,8 +3,8 @@ function Set-CIPPNamedLocation { param( $NamedLocationId, $TenantFilter, - #$Change should be one of 'addIp','addLocation','removeIp','removeLocation','rename','setTrusted','setUntrusted' - [ValidateSet('addIp', 'addLocation', 'removeIp', 'removeLocation', 'rename', 'setTrusted', 'setUntrusted')] + #$Change should be one of 'addIp','addLocation','removeIp','removeLocation','rename','setTrusted','setUntrusted','delete' + [ValidateSet('addIp', 'addLocation', 'removeIp', 'removeLocation', 'rename', 'setTrusted', 'setUntrusted', 'delete')] $Change, $Content, $APIName = 'Set Named Location', @@ -13,40 +13,58 @@ function Set-CIPPNamedLocation { try { $NamedLocations = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations/$NamedLocationId" -Tenantid $TenantFilter + switch ($Change) { 'addIp' { $NamedLocations.ipRanges = @($NamedLocations.ipRanges + @{ cidrAddress = $Content; '@odata.type' = '#microsoft.graph.iPv4CidrRange' }) + $ActionDescription = "Adding IP $Content to named location" } 'addLocation' { $NamedLocations.countriesAndRegions = $NamedLocations.countriesAndRegions + $Content + $ActionDescription = "Adding location $Content to named location" } 'removeIp' { $NamedLocations.ipRanges = @($NamedLocations.ipRanges | Where-Object -Property cidrAddress -NE $Content) + $ActionDescription = "Removing IP $Content from named location" } 'removeLocation' { $NamedLocations.countriesAndRegions = @($NamedLocations.countriesAndRegions | Where-Object { $_ -NE $Content }) + $ActionDescription = "Removing location $Content from named location" } 'rename' { $NamedLocations.displayName = $Content + $ActionDescription = "Renaming named location to: $Content" } 'setTrusted' { $NamedLocations.isTrusted = $true + $ActionDescription = 'Setting named location as trusted' } 'setUntrusted' { $NamedLocations.isTrusted = $false + $ActionDescription = 'Setting named location as untrusted' + } + 'delete' { + $ActionDescription = 'Deleting named location' } } - if ($PSCmdlet.ShouldProcess($NamedLocations.displayName, "Editing named location: $($NamedLocations.displayName). Change: $Change with content $($Content)")) { - #Remove unneeded properties - if ($NamedLocations.'@odata.type' -eq '#microsoft.graph.countryNamedLocation') { - $NamedLocations = $NamedLocations | Select-Object '@odata.type', 'displayName', 'countriesAndRegions', 'includeUnknownCountriesAndRegions' - } elseif ($NamedLocations.'@odata.type' -eq '#microsoft.graph.ipNamedLocation') { - $NamedLocations = $NamedLocations | Select-Object '@odata.type', 'displayName', 'ipRanges', 'isTrusted' + + if ($PSCmdlet.ShouldProcess($NamedLocations.displayName, $ActionDescription)) { + if ($Change -eq 'delete') { + $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations/$NamedLocationId" -tenantid $TenantFilter -type DELETE + $Result = "Deleted named location: $($NamedLocations.displayName)" + } else { + # PATCH operations - remove unneeded properties + if ($NamedLocations.'@odata.type' -eq '#microsoft.graph.countryNamedLocation') { + $NamedLocations = $NamedLocations | Select-Object '@odata.type', 'displayName', 'countriesAndRegions', 'includeUnknownCountriesAndRegions' + } elseif ($NamedLocations.'@odata.type' -eq '#microsoft.graph.ipNamedLocation') { + $NamedLocations = $NamedLocations | Select-Object '@odata.type', 'displayName', 'ipRanges', 'isTrusted' + } + + $JsonBody = ConvertTo-Json -InputObject $NamedLocations -Compress -Depth 10 + $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations/$NamedLocationId" -tenantid $TenantFilter -type PATCH -body $JsonBody + $Result = "Edited named location: $($NamedLocations.displayName). Change: $Change$(if ($Content) { " with content $Content" })" } - $JsonBody = ConvertTo-Json -InputObject $NamedLocations -Compress -Depth 10 - $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations/$NamedLocationId" -tenantid $TenantFilter -type PATCH -body $JsonBody - $Result = "Edited named location: $($NamedLocations.displayName). Change: $Change with content $($Content)" Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev 'Info' } return $Result From 6a3dae3d0b93d6c118b6e07a2b8baf074acaed7b Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 13 Jun 2025 13:42:16 +0200 Subject: [PATCH 087/160] move permission to tenant groups --- .../CIPP/Settings/Invoke-ExecTenantGroup.ps1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecTenantGroup.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecTenantGroup.ps1 index d0ec882cc7ba..029f21fe25be 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecTenantGroup.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecTenantGroup.ps1 @@ -7,7 +7,7 @@ function Invoke-ExecTenantGroup { .FUNCTIONALITY Entrypoint,AnyTenant .ROLE - Tenant.Config.ReadWrite + TenantGroups.Config.ReadWrite #> [CmdletBinding()] param($Request, $TriggerMetadata) @@ -57,9 +57,9 @@ function Invoke-ExecTenantGroup { } $MemberEntity = @{ PartitionKey = 'Member' - RowKey = '{0}-{1}' -f $groupId, $member.value - GroupId = $groupId - customerId = $member.value + RowKey = '{0}-{1}' -f $groupId, $member.value + GroupId = $groupId + customerId = $member.value } Add-CIPPAzDataTableEntity @MembersTable -Entity $MemberEntity -Force $Adds.Add('Added member {0}' -f $member.label) From 2a3e57c65b2bba6677c4b6faaee5fec0c2b245e9 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 13 Jun 2025 13:48:02 +0200 Subject: [PATCH 088/160] added permission --- .../Application Approval/Invoke-ExecAppPermissionTemplate.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecAppPermissionTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecAppPermissionTemplate.ps1 index 4c92415403d2..4dcabae9c18a 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecAppPermissionTemplate.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecAppPermissionTemplate.ps1 @@ -3,7 +3,7 @@ function Invoke-ExecAppPermissionTemplate { .FUNCTIONALITY Entrypoint,AnyTenant .ROLE - Tenant.Application.ReadWrite + Tenant.ApplicationTemplates.ReadWrite #> [CmdletBinding()] param($Request, $TriggerMetadata) From e2a4024d0f990eca329bce44ca0142bf79d35e46 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Fri, 13 Jun 2025 12:59:02 +0100 Subject: [PATCH 089/160] Add bobbytables to profile import --- profile.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/profile.ps1 b/profile.ps1 index cfcf29575171..a763fe8b693e 100644 --- a/profile.ps1 +++ b/profile.ps1 @@ -13,7 +13,7 @@ # Remove this if you are not planning on using MSI or Azure PowerShell. # Import modules -@('CIPPCore', 'CippExtensions', 'Az.KeyVault', 'Az.Accounts') | ForEach-Object { +@('CIPPCore', 'CippExtensions', 'Az.KeyVault', 'Az.Accounts', 'AzBobbyTables') | ForEach-Object { try { $Module = $_ Import-Module -Name $_ -ErrorAction Stop From 7484b9cd5ebc18649c9d5946b8061ea4606fbb37 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 13 Jun 2025 14:06:53 +0200 Subject: [PATCH 090/160] fixes issue with assigned app --- .../HTTP Functions/Endpoint/Applications/Invoke-AddChocoApp.ps1 | 2 +- Modules/CIPPCore/Public/Set-CIPPAssignedApplication.ps1 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddChocoApp.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddChocoApp.ps1 index a212a7111946..8037b40239f5 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddChocoApp.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddChocoApp.ps1 @@ -16,7 +16,7 @@ Function Invoke-AddChocoApp { $ChocoApp = $Request.Body $intuneBody = Get-Content 'AddChocoApp\Choco.app.json' | ConvertFrom-Json - $AssignTo = $Request.Body.AssignTo + $AssignTo = $Request.Body.AssignTo -eq 'customGroup' ? $Request.Body.CustomGroup : $Request.Body.AssignTo $intuneBody.description = $ChocoApp.description $intuneBody.displayName = $ChocoApp.ApplicationName $intuneBody.installExperience.runAsAccount = if ($ChocoApp.InstallAsSystem) { 'system' } else { 'user' } diff --git a/Modules/CIPPCore/Public/Set-CIPPAssignedApplication.ps1 b/Modules/CIPPCore/Public/Set-CIPPAssignedApplication.ps1 index 478f1aa6e2b9..272c0d21aff8 100644 --- a/Modules/CIPPCore/Public/Set-CIPPAssignedApplication.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPAssignedApplication.ps1 @@ -9,7 +9,7 @@ function Set-CIPPAssignedApplication { $APIName = 'Assign Application', $Headers ) - + Write-Host "GroupName: $GroupName Intent: $Intent AppType: $AppType ApplicationId: $ApplicationId TenantFilter: $TenantFilter APIName: $APIName" try { $MobileAppAssignment = switch ($GroupName) { 'AllUsers' { From 56c5691125447cc1fb5646275dafb0ecc0f5e880 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Fri, 13 Jun 2025 13:54:44 +0100 Subject: [PATCH 091/160] Add CA policy displayname change action --- .../Conditional/Invoke-EditCAPolicy.ps1 | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-EditCAPolicy.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-EditCAPolicy.ps1 index e8d3d5692a54..eb3324e2ab75 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-EditCAPolicy.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-EditCAPolicy.ps1 @@ -18,16 +18,31 @@ Function Invoke-EditCAPolicy { $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter $ID = $Request.Query.GUID ?? $Request.Body.GUID $State = $Request.Query.State ?? $Request.Body.State + $DisplayName = $Request.Query.newDisplayName ?? $Request.Body.newDisplayName try { - $EditBody = "{`"state`": `"$($State)`"}" - $Request = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta//identity/conditionalAccess/policies/$($ID)" -tenantid $TenantFilter -type PATCH -body $EditBody -asapp $true - $Result = "Successfully set CA policy $($ID) to $($State)" + $properties = @{} + + # Conditionally add properties + if ($State) { + $properties["state"] = $State + } + + if ($DisplayName) { + $properties["displayName"] = $DisplayName + } + + $Request = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta//identity/conditionalAccess/policies/$($ID)" -tenantid $TenantFilter -type PATCH -body ($properties | ConvertTo-Json) -asapp $true + + $Result = "Successfully updated CA policy $($ID)" + if ($State) { $Result += " state to $($State)" } + if ($DisplayName) { $Result += " name to '$($DisplayName)'" } + Write-LogMessage -headers $Headers -API $APIName -tenant $($TenantFilter) -message $Result -Sev 'Info' $StatusCode = [HttpStatusCode]::OK } catch { $ErrorMessage = Get-CippException -Exception $_ - $Result = "Failed to set CA policy $($ID) to $($State): $($ErrorMessage.NormalizedError)" + $Result = "Failed to update CA policy $($ID): $($ErrorMessage.NormalizedError)" Write-LogMessage -headers $Headers -API $APIName -tenant $($TenantFilter) -message $Result -Sev 'Error' -LogData $ErrorMessage $StatusCode = [HttpStatusCode]::InternalServerError } From 30069dc56a0c28f39513866aeb8b8a7467a679b3 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Fri, 13 Jun 2025 14:50:08 +0100 Subject: [PATCH 092/160] API support for editing Intune policy displaynames --- .../Endpoint/MEM/Invoke-EditIntunePolicy.ps1 | 51 +++++++++++++++++++ .../Endpoint/MEM/Invoke-EditPolicy.ps1 | 2 + 2 files changed, 53 insertions(+) create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-EditIntunePolicy.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-EditIntunePolicy.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-EditIntunePolicy.ps1 new file mode 100644 index 000000000000..1d34f8196a72 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-EditIntunePolicy.ps1 @@ -0,0 +1,51 @@ +using namespace System.Net + +Function Invoke-EditIntunePolicy { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Endpoint.MEM.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + + # Interact with query parameters or the body of the request. + $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter + $ID = $Request.Query.ID ?? $Request.Body.ID + $DisplayName = $Request.Query.newDisplayName ?? $Request.Body.newDisplayName + $PolicyType = $Request.Query.policyType ?? $Request.Body.policyType + + try { + $properties = @{} + + # Only add displayName if it's provided + if ($DisplayName) { + $properties["displayName"] = $DisplayName + } + + # Update the policy + $Request = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$PolicyType/$ID" -tenantid $TenantFilter -type PATCH -body ($properties | ConvertTo-Json) -asapp $true + + $Result = "Successfully updated Intune policy $($ID)" + if ($DisplayName) { $Result += " name to '$($DisplayName)'" } + + Write-LogMessage -headers $Headers -API $APIName -tenant $($TenantFilter) -message $Result -Sev 'Info' + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to update Intune policy $($ID): $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -tenant $($TenantFilter) -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 = @{ 'Results' = $Result } + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-EditPolicy.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-EditPolicy.ps1 index 85a0572a77cf..53586dd663c2 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-EditPolicy.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-EditPolicy.ps1 @@ -10,6 +10,8 @@ Function Invoke-EditPolicy { [CmdletBinding()] param($Request, $TriggerMetadata) + # Note, suspect this is deprecated - rvdwegen + $APIName = $Request.Params.CIPPEndpoint $Headers = $Request.Headers Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' From 40c164c9fd0144367ea9911b9aeb42a4c6eaf03f Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Fri, 13 Jun 2025 16:48:43 +0100 Subject: [PATCH 093/160] Add standard to set default SP & Onedrive sharing #4234 --- .../Invoke-CIPPStandardDefaultSharingLink.ps1 | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDefaultSharingLink.ps1 diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDefaultSharingLink.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDefaultSharingLink.ps1 new file mode 100644 index 000000000000..9093ab528e51 --- /dev/null +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDefaultSharingLink.ps1 @@ -0,0 +1,73 @@ +function Invoke-CIPPStandardDefaultSharingLink { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) DefaultSharingLink + .SYNOPSIS + (Label) Set Default Sharing Link Settings + .DESCRIPTION + (Helptext) Sets the default sharing link type to Internal and permission to View in SharePoint and OneDrive. + (DocsDescription) Sets the default sharing link type to Internal and permission to View in SharePoint and OneDrive. + .NOTES + CAT + SharePoint Standards + TAG + ADDEDCOMPONENT + IMPACT + Medium Impact + ADDEDDATE + 2025-06-13 + POWERSHELLEQUIVALENT + Set-SPOTenant -DefaultSharingLinkType Internal -DefaultLinkPermission View + RECOMMENDEDBY + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + .LINK + https://docs.cipp.app/user-documentation/tenant/standards/list-standards + #> + + param($Tenant, $Settings) + + $CurrentState = Get-CIPPSPOTenant -TenantFilter $Tenant | + Select-Object -Property DefaultSharingLinkType, DefaultLinkPermission + + $StateIsCorrect = ($CurrentState.DefaultSharingLinkType -eq 2) -and ($CurrentState.DefaultLinkPermission -eq 1) + + if ($Settings.remediate -eq $true) { + if ($StateIsCorrect -eq $true) { + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'Default sharing link settings are already configured correctly' -Sev Info + } else { + $Properties = @{ + DefaultSharingLinkType = 2 # Internal + DefaultLinkPermission = 1 # View + } + + try { + Get-CIPPSPOTenant -TenantFilter $Tenant | Set-CIPPSPOTenant -Properties $Properties + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'Successfully set default sharing link settings' -Sev Info + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Failed to set default sharing link settings. Error: $ErrorMessage" -Sev Error + } + } + } + + if ($Settings.alert -eq $true) { + if ($StateIsCorrect -eq $true) { + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'Default sharing link settings are configured correctly' -Sev Info + } else { + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'Default sharing link settings are not configured correctly' -Sev Alert + } + } + + if ($Settings.report -eq $true) { + Add-CIPPBPAField -FieldName 'DefaultSharingLink' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant + if ($StateIsCorrect) { + $FieldValue = $true + } else { + $FieldValue = $CurrentState + } + Set-CIPPStandardsCompareField -FieldName 'standards.DefaultSharingLink' -FieldValue $FieldValue -Tenant $Tenant + } +} From 6b6e7e23c540323ac0442211dd11b780ce87a58e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Fri, 13 Jun 2025 18:52:48 +0200 Subject: [PATCH 094/160] Feat: reenable unmanagedSync standard --- .../Invoke-CIPPStandardunmanagedSync.ps1 | 47 +++++++++++-------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardunmanagedSync.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardunmanagedSync.ps1 index a84306c4a549..57fe9a533670 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardunmanagedSync.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardunmanagedSync.ps1 @@ -5,22 +5,24 @@ function Invoke-CIPPStandardunmanagedSync { .COMPONENT (APIName) unmanagedSync .SYNOPSIS - (Label) Only allow users to sync OneDrive from AAD joined devices + (Label) Restrict access to SharePoint and OneDrive from unmanaged devices .DESCRIPTION - (Helptext) The unmanaged Sync standard has been temporarily disabled and does nothing. - (DocsDescription) The unmanaged Sync standard has been temporarily disabled and does nothing. + (Helptext) Entra P1 required. Block or limit access to SharePoint and OneDrive content from unmanaged devices (those not hybrid AD joined or compliant in Intune). These controls rely on Microsoft Entra Conditional Access policies and can take up to 24 hours to take effect. + (DocsDescription) Entra P1 required. Block or limit access to SharePoint and OneDrive content from unmanaged devices (those not hybrid AD joined or compliant in Intune). These controls rely on Microsoft Entra Conditional Access policies and can take up to 24 hours to take effect. 0 = Allow Access, 1 = Allow limited, web-only access, 2 = Block access. All information about this can be found in Microsofts documentation [here.](https://learn.microsoft.com/en-us/sharepoint/control-access-from-unmanaged-devices) .NOTES CAT SharePoint Standards TAG ADDEDCOMPONENT + {"type":"autoComplete","multiple":false,"creatable":false,"name":"standards.unmanagedSync.state","label":"State","options":[{"label":"Allow limited, web-only access","value":"1"},{"label":"Block access","value":"2"}],"required":false} IMPACT High Impact ADDEDDATE - 2022-06-15 + 2025-06-13 POWERSHELLEQUIVALENT - Update-MgAdminSharePointSetting + Set-SPOTenant -ConditionalAccessPolicy AllowFullAccess \| AllowLimitedAccess \| BlockAccess RECOMMENDEDBY + "CIS" UPDATECOMMENTBLOCK Run the Tools\Update-StandardsComments.ps1 script to update this comment block .LINK @@ -30,36 +32,41 @@ function Invoke-CIPPStandardunmanagedSync { param($Tenant, $Settings) ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'unmanagedSync' - $CurrentInfo = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/admin/sharepoint/settings' -tenantid $Tenant -AsApp $true + $CurrentState = Get-CIPPSPOTenant -TenantFilter $Tenant | Select-Object _ObjectIdentity_, TenantFilter, ConditionalAccessPolicy + + $WantedState = [int]($Settings.state.value ?? 2) # Default to 2 (Block Access) if not set, for pre v8.0.3 standard compatibility + $Label = $Settings.state.label ?? 'Block Access' # Default label if not set, for pre v8.0.3 standard compatibility + $StateIsCorrect = ($CurrentState.ConditionalAccessPolicy -eq $WantedState) If ($Settings.remediate -eq $true) { - if ($CurrentInfo.isUnmanagedSyncAppForTenantRestricted -eq $false) { + if ($StateIsCorrect -eq $true) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Sync for unmanaged devices is already correctly set to: $Label" -sev Info + } else { try { - #$body = '{"isUnmanagedSyncAppForTenantRestricted": true}' - #$null = New-GraphPostRequest -tenantid $tenant -Uri 'https://graph.microsoft.com/beta/admin/sharepoint/settings' -AsApp $true -Type patch -Body $body -ContentType 'application/json' - Write-LogMessage -API 'Standards' -tenant $tenant -message 'The unmanaged Sync standard has been temporarily disabled.' -sev Info + $CurrentState | Set-CIPPSPOTenant -Properties @{ConditionalAccessPolicy = $WantedState } + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully set the unmanaged Sync state to: $Label" -sev Info } catch { - $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to disable Sync for unmanaged devices: $ErrorMessage" -sev Error + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to disable Sync for unmanaged devices: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage } - } else { - Write-LogMessage -API 'Standards' -tenant $tenant -message 'Sync for unmanaged devices is already disabled' -sev Info } } if ($Settings.alert -eq $true) { - if ($CurrentInfo.isUnmanagedSyncAppForTenantRestricted -eq $true) { - Write-LogMessage -API 'Standards' -tenant $tenant -message 'Sync for unmanaged devices is disabled' -sev Info + if ($StateIsCorrect -eq $true) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Sync for unmanaged devices is correctly set to: $Label" -sev Info } else { - Write-StandardsAlert -message 'Sync for unmanaged devices is not disabled' -object $CurrentInfo -tenant $tenant -standardName 'unmanagedSync' -standardId $Settings.standardId - Write-LogMessage -API 'Standards' -tenant $tenant -message 'Sync for unmanaged devices is not disabled' -sev Info + Write-StandardsAlert -message "Sync for unmanaged devices is not correctly set to $Label, but instead $($CurrentState.ConditionalAccessPolicy)" -object $CurrentState -tenant $Tenant -standardName 'unmanagedSync' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Sync for unmanaged devices is not correctly set to $Label, but instead $($CurrentState.ConditionalAccessPolicy)" -sev Info } } if ($Settings.report -eq $true) { - Set-CIPPStandardsCompareField -FieldName 'standards.unmanagedSync' -FieldValue $CurrentInfo.isUnmanagedSyncAppForTenantRestricted -Tenant $tenant - Add-CIPPBPAField -FieldName 'unmanagedSync' -FieldValue $CurrentInfo.isUnmanagedSyncAppForTenantRestricted -StoreAs bool -Tenant $tenant + + $State = $StateIsCorrect ? $true : $CurrentState.ConditionalAccessPolicy + Set-CIPPStandardsCompareField -FieldName 'standards.unmanagedSync' -FieldValue $State -Tenant $Tenant + Add-CIPPBPAField -FieldName 'unmanagedSync' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant } } From 5f725720bcca6adc1a31978a29ed203aa60ee181 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Fri, 13 Jun 2025 18:52:59 +0200 Subject: [PATCH 095/160] chore: update comments --- .../Invoke-CIPPStandardAutopilotProfile.ps1 | 2 +- ...oke-CIPPStandardDeployContactTemplates.ps1 | 14 +++++----- .../Invoke-CIPPStandardDeployMailContact.ps1 | 6 +++- ...ke-CIPPStandardEnableNamePronunciation.ps1 | 1 + ...ke-CIPPStandardFormsPhishingProtection.ps1 | 13 +++++---- ...CIPPStandardPWcompanionAppAllowedState.ps1 | 2 +- ...ndardRestrictThirdPartyStorageServices.ps1 | 8 +++--- ...ke-CIPPStandardSafeLinksTemplatePolicy.ps1 | 28 +++++++++---------- ...voke-CIPPStandardUserPreferredLanguage.ps1 | 2 +- 9 files changed, 41 insertions(+), 35 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotProfile.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotProfile.ps1 index cf616b421872..549c0e866a9d 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotProfile.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotProfile.ps1 @@ -18,7 +18,7 @@ function Invoke-CIPPStandardAutopilotProfile { ADDEDCOMPONENT {"type":"textField","name":"standards.AutopilotProfile.DisplayName","label":"Profile Display Name"} {"type":"textField","name":"standards.AutopilotProfile.Description","label":"Profile Description"} - {"type":"textField","name":"standards.AutopilotProfile.DeviceNameTemplate","label":"Unique Device Name Template"} + {"type":"textField","name":"standards.AutopilotProfile.DeviceNameTemplate","label":"Unique Device Name Template","required":false} {"type":"autoComplete","multiple":false,"creatable":false,"required":false,"name":"standards.AutopilotProfile.Languages","label":"Languages","api":{"url":"/languageList.json","labelField":"language","valueField":"tag"}} {"type":"switch","name":"standards.AutopilotProfile.CollectHash","label":"Convert all targeted devices to Autopilot","defaultValue":true} {"type":"switch","name":"standards.AutopilotProfile.AssignToAllDevices","label":"Assign to all devices","defaultValue":true} diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 index 692a1eecbc64..26a3f61a0d12 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 @@ -5,22 +5,22 @@ function Invoke-CIPPStandardDeployContactTemplates { .COMPONENT (APIName) DeployContactTemplates .SYNOPSIS - (Label) Deploy Contact Templates + (Label) Deploy Mail Contact Template .DESCRIPTION - (Helptext) Creates a new contacts in Exchange Online across all selected tenants from saved contact templates. The contact will be visible in the Global Address List unless hidden. - (DocsDescription) This standard creates new contacts in Exchange Online from saved contact templates. Mail contacts are useful for adding external email addresses to your organization's address book. They can be used for distribution lists, shared mailboxes, and other collaboration scenarios. + (Helptext) Creates new mail contacts in Exchange Online across all selected tenants based on the selected templates. The contact will be visible in the Global Address List unless hidden. + (DocsDescription) This standard creates new mail contacts in Exchange Online based on the selected templates. Mail contacts are useful for adding external email addresses to your organization's address book. They can be used for distribution lists, shared mailboxes, and other collaboration scenarios. .NOTES CAT Exchange Standards TAG ADDEDCOMPONENT - {"type":"textField","name":"TemplateGUID","label":"Contact Template GUID","required":true} - MULTIPLE - True + {"type":"autoComplete","multiple":true,"creatable":false,"label":"Select Mail Contact Templates","name":"standards.DeployContactTemplates.templateIds","api":{"url":"/api/ListContactTemplates","labelField":"name","valueField":"GUID","queryKey":"Contact Templates"}} + DISABLEDFEATURES + {"report":false,"warn":false,"remediate":false} IMPACT Low Impact ADDEDDATE - 2024-03-19 + 2025-05-31 POWERSHELLEQUIVALENT New-MailContact RECOMMENDEDBY diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1 index 9a3b212639c9..4f991c25be1f 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1 @@ -21,11 +21,15 @@ function Invoke-CIPPStandardDeployMailContact { IMPACT Low Impact ADDEDDATE - 2025-05-28 + 2024-03-19 POWERSHELLEQUIVALENT New-MailContact RECOMMENDEDBY "CIPP" + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + .LINK + https://docs.cipp.app/user-documentation/tenant/standards/list-standards #> param($Tenant, $Settings) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableNamePronunciation.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableNamePronunciation.ps1 index 6bf485ebb722..b3d00a681c95 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableNamePronunciation.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableNamePronunciation.ps1 @@ -19,6 +19,7 @@ function Invoke-CIPPStandardEnableNamePronunciation { ADDEDDATE 2025-06-06 RECOMMENDEDBY + "CIPP" UPDATECOMMENTBLOCK Run the Tools\Update-StandardsComments.ps1 script to update this comment block .LINK diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardFormsPhishingProtection.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardFormsPhishingProtection.ps1 index cea83fe112cf..795216b19e74 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardFormsPhishingProtection.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardFormsPhishingProtection.ps1 @@ -5,24 +5,27 @@ function Invoke-CIPPStandardFormsPhishingProtection { .COMPONENT (APIName) FormsPhishingProtection .SYNOPSIS - (Label) Ensure internal phishing protection for Forms is enabled + (Label) Enable internal phishing protection for Forms .DESCRIPTION (Helptext) Enables internal phishing protection for Microsoft Forms to help prevent malicious forms from being created and shared within the organization. This feature scans forms created by internal users for potential phishing content and suspicious patterns. (DocsDescription) Enables internal phishing protection for Microsoft Forms by setting the isInOrgFormsPhishingScanEnabled property to true. This security feature helps protect organizations from internal phishing attacks through Microsoft Forms by automatically scanning forms created by internal users for potential malicious content, suspicious links, and phishing patterns. When enabled, Forms will analyze form content and block or flag potentially dangerous forms before they can be shared within the organization. .NOTES CAT - Defender Standards + Global Standards TAG - "CIS", "Security", "PhishingProtection" + "CIS" + "Security" + "PhishingProtection" ADDEDCOMPONENT IMPACT Low Impact ADDEDDATE - 2025-01-27 + 2025-06-06 POWERSHELLEQUIVALENT - Set-FormsSettings -isInOrgFormsPhishingScanEnabled $true + Graph API RECOMMENDEDBY "CIS" + "CIPP" UPDATECOMMENTBLOCK Run the Tools\Update-StandardsComments.ps1 script to update this comment block .LINK diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPWcompanionAppAllowedState.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPWcompanionAppAllowedState.ps1 index 2322f74afe7f..563472eec41f 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPWcompanionAppAllowedState.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPWcompanionAppAllowedState.ps1 @@ -14,7 +14,7 @@ function Invoke-CIPPStandardPWcompanionAppAllowedState { Entra (AAD) Standards TAG ADDEDCOMPONENT - {"type":"autoComplete","multiple":false,"creatable":false,"label":"Select value","name":"standards.PWcompanionAppAllowedState.state","options":[{"label":"Enabled","value":"enabled"},{"label":"Disabled","value":"disabled"}]} + {"type":"autoComplete","multiple":false,"creatable":false,"label":"Select value","name":"standards.PWcompanionAppAllowedState.state","options":[{"label":"Enabled","value":"enabled"},{"label":"Disabled","value":"disabled"},{"label":"Microsoft managed","value":"default"}]} IMPACT Low Impact ADDEDDATE diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRestrictThirdPartyStorageServices.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRestrictThirdPartyStorageServices.ps1 index 8da44190ffac..d523b3d83174 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRestrictThirdPartyStorageServices.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRestrictThirdPartyStorageServices.ps1 @@ -5,10 +5,10 @@ function Invoke-CIPPStandardRestrictThirdPartyStorageServices { .COMPONENT (APIName) RestrictThirdPartyStorageServices .SYNOPSIS - (Label) Restrict Third-Party Storage Services in Microsoft 365 on the web + (Label) Restrict third-party storage services in Microsoft 365 on the web .DESCRIPTION - (Helptext) Ensures that third-party storage services are restricted in Microsoft 365 on the web. This disables the ability for users to connect external storage providers like Dropbox, Google Drive, etc. through the Office 365 web interface. - (DocsDescription) Ensures that third-party storage services are restricted in Microsoft 365 on the web. This disables the ability for users to connect external storage providers like Dropbox, Google Drive, etc. through the Office 365 web interface by disabling the Microsoft 365 on the web service principal. + (Helptext) Restricts third-party storage services in Microsoft 365 on the web by managing the Microsoft 365 on the web service principal. This disables integrations with services like Dropbox, Google Drive, Box, and other third-party storage providers. + (DocsDescription) Third-party storage can be enabled for users in Microsoft 365, allowing them to store and share documents using services such as Dropbox, alongside OneDrive and team sites. This standard ensures Microsoft 365 on the web third-party storage services are restricted by creating and disabling the Microsoft 365 on the web service principal (appId: c1f33bc0-bdb4-4248-ba9b-096807ddb43e). By using external storage services an organization may increase the risk of data breaches and unauthorized access to confidential information. Additionally, third-party services may not adhere to the same security standards as the organization, making it difficult to maintain data privacy and security. Impact is highly dependent upon current practices - if users do not use other storage providers, then minimal impact is likely. However, if users regularly utilize providers outside of the tenant this will affect their ability to continue to do so. .NOTES CAT Global Standards @@ -20,7 +20,7 @@ function Invoke-CIPPStandardRestrictThirdPartyStorageServices { ADDEDDATE 2025-06-06 POWERSHELLEQUIVALENT - Get-AzureADServicePrincipal -Filter "appId eq 'c1f33bc0-bdb4-4248-ba9b-096807ddb43e'" | Set-AzureADServicePrincipal -AccountEnabled \$false + New-MgServicePrincipal and Update-MgServicePrincipal RECOMMENDEDBY "CIS" UPDATECOMMENTBLOCK diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksTemplatePolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksTemplatePolicy.ps1 index e630e12a900b..00d71e371b2b 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksTemplatePolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksTemplatePolicy.ps1 @@ -7,27 +7,25 @@ function Invoke-CIPPStandardSafeLinksTemplatePolicy { .SYNOPSIS (Label) SafeLinks Policy Template .DESCRIPTION - (Helptext) This applies selected SafeLinks policy templates to the tenant, creating or updating as needed - (DocsDescription) This applies selected SafeLinks policy templates to the tenant, creating or updating as needed + (Helptext) Deploy and manage SafeLinks policy templates to protect against malicious URLs in emails and Office documents. + (DocsDescription) Deploy and manage SafeLinks policy templates to protect against malicious URLs in emails and Office documents. .NOTES CAT - Defender Standards - TAG - "CIS" - "mdo_safelinksforemail" - "mdo_safelinksforOfficeApps" - ADDEDCOMPONENT - {"type":"autoComplete","multiple":true,"name":"standards.SafeLinksTemplatePolicy.TemplateIds","label":"SafeLinks Templates","loadingMessage":"Loading templates...","api":{"url":"/api/ListSafeLinksPolicyTemplates","labelField":"name","valueField":"GUID","queryKey":"ListSafeLinksPolicyTemplates"}} + Templates + MULTIPLE + False + DISABLEDFEATURES + {"report":false,"warn":false,"remediate":false} IMPACT - Low Impact + Medium Impact ADDEDDATE 2025-04-29 - POWERSHELLEQUIVALENT - New-SafeLinksPolicy, Set-SafeLinksPolicy, New-SafeLinksRule, Set-SafeLinksRule - RECOMMENDEDBY - "CIS" + ADDEDCOMPONENT + {"type":"autoComplete","multiple":true,"creatable":false,"name":"standards.SafeLinksTemplatePolicy.TemplateIds","label":"Select SafeLinks Policy Templates","api":{"url":"/api/ListSafeLinksPolicyTemplates","labelField":"TemplateName","valueField":"GUID","queryKey":"ListSafeLinksPolicyTemplates"}} + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block .LINK - https://docs.cipp.app/user-documentation/tenant/standards/list-standards/defender-standards#low-impact + https://docs.cipp.app/user-documentation/tenant/standards/list-standards #> param($Tenant, $Settings) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserPreferredLanguage.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserPreferredLanguage.ps1 index 8d78372a969d..e2b65e9c9bba 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserPreferredLanguage.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserPreferredLanguage.ps1 @@ -14,7 +14,7 @@ function Invoke-CIPPStandardUserPreferredLanguage { Entra (AAD) Standards TAG ADDEDCOMPONENT - {"type":"autoComplete","multiple":false,"creatable":false,"name":"standards.UserPreferredLanguage.preferredLanguage","label":"Preferred Language","api":{"url":"/languageList.json","labelField":"language","valueField":"tag"}} + {"type":"autoComplete","multiple":false,"creatable":false,"name":"standards.UserPreferredLanguage.preferredLanguage","label":"Preferred Language","api":{"url":"/languageList.json","labelField":"tag","valueField":"tag"}} IMPACT High Impact ADDEDDATE From 6ce06bd10e7da552d7b2600f30ea009010211024 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Fri, 13 Jun 2025 20:45:30 +0200 Subject: [PATCH 096/160] Feat: Add TwoClickEmailProtection standard --- ...ke-CIPPStandardTwoClickEmailProtection.ps1 | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTwoClickEmailProtection.ps1 diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTwoClickEmailProtection.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTwoClickEmailProtection.ps1 new file mode 100644 index 000000000000..9f80dfe4c0a3 --- /dev/null +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTwoClickEmailProtection.ps1 @@ -0,0 +1,83 @@ +function Invoke-CIPPStandardTwoClickEmailProtection { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) TwoClickEmailProtection + .SYNOPSIS + (Label) Set two-click confirmation for encrypted emails in New Outlook + .DESCRIPTION + (Helptext) Configures the two-click confirmation requirement for viewing encrypted/protected emails in OWA and new Outlook. When enabled, users must click "View message" before accessing protected content, providing an additional layer of privacy protection. + (DocsDescription) Configures the TwoClickMailPreviewEnabled setting in Exchange Online organization configuration. This security feature requires users to click "View message" before accessing encrypted or protected emails in Outlook on the web (OWA) and new Outlook for Windows. This provides additional privacy protection by preventing protected content from automatically displaying, giving users time to ensure their screen is not visible to others before viewing sensitive content. The feature helps protect against shoulder surfing and accidental disclosure of confidential information. + .NOTES + CAT + Exchange Standards + TAG + ADDEDCOMPONENT + {"type":"autoComplete","multiple":false,"creatable":false,"label":"Select value","name":"standards.TwoClickEmailProtection.state","options":[{"label":"Enabled","value":"enabled"},{"label":"Disabled","value":"disabled"}]} + IMPACT + Low Impact + ADDEDDATE + 2025-06-13 + POWERSHELLEQUIVALENT + Set-OrganizationConfig -TwoClickMailPreviewEnabled \$true \| \$false + RECOMMENDEDBY + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + .LINK + https://docs.cipp.app/user-documentation/tenant/standards/list-standards + #> + + param($Tenant, $Settings) + ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'TwoClickEmailProtection' + + # Get state value using null-coalescing operator + $state = $Settings.state.value ?? $Settings.state + + # Input validation + if ([string]::IsNullOrWhiteSpace($state)) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'TwoClickEmailProtection: Invalid state parameter set' -sev Error + Return + } + + try { + $CurrentState = (New-ExoRequest -tenantid $Tenant -cmdlet 'Get-OrganizationConfig').TwoClickMailPreviewEnabled + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Could not get current two-click email protection state. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Return + } + + $WantedState = $state -eq 'enabled' ? $true : $false + $StateIsCorrect = $CurrentState -eq $WantedState ? $true : $false + + if ($Settings.remediate -eq $true) { + Write-Host 'Time to remediate two-click email protection' + + if ($StateIsCorrect -eq $true) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Two-click email protection is already set to $state." -sev Info + } else { + try { + $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-OrganizationConfig' -cmdParams @{ TwoClickMailPreviewEnabled = $WantedState } -useSystemMailbox $true + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully set two-click email protection to $state." -sev Info + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to set two-click email protection to $state. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + } + } + } + + if ($Settings.alert -eq $true) { + if ($StateIsCorrect -eq $true) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Two-click email protection is correctly set to $state." -sev Info + } else { + Write-StandardsAlert -message "Two-click email protection is not correctly set to $state, but instead $($CurrentState ? 'enabled' : 'disabled')" -object @{TwoClickMailPreviewEnabled = $CurrentState } -tenant $Tenant -standardName 'TwoClickEmailProtection' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Two-click email protection is not correctly set to $state, but instead $($CurrentState ? 'enabled' : 'disabled')" -sev Info + } + } + + if ($Settings.report -eq $true) { + Set-CIPPStandardsCompareField -FieldName 'standards.TwoClickEmailProtection' -FieldValue $StateIsCorrect -Tenant $Tenant + Add-CIPPBPAField -FieldName 'TwoClickEmailProtection' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant + } +} From a82b206e89c8df6c9b36662ca314c30c2cdb7a25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Fri, 13 Jun 2025 20:51:56 +0200 Subject: [PATCH 097/160] fix: casing and instant reporting --- ...ke-CIPPStandardTwoClickEmailProtection.ps1 | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTwoClickEmailProtection.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTwoClickEmailProtection.ps1 index 9f80dfe4c0a3..13cbb9562d82 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTwoClickEmailProtection.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTwoClickEmailProtection.ps1 @@ -32,10 +32,10 @@ function Invoke-CIPPStandardTwoClickEmailProtection { ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'TwoClickEmailProtection' # Get state value using null-coalescing operator - $state = $Settings.state.value ?? $Settings.state + $State = $Settings.state.value ?? $Settings.state # Input validation - if ([string]::IsNullOrWhiteSpace($state)) { + if ([string]::IsNullOrWhiteSpace($State)) { Write-LogMessage -API 'Standards' -tenant $Tenant -message 'TwoClickEmailProtection: Invalid state parameter set' -sev Error Return } @@ -48,31 +48,32 @@ function Invoke-CIPPStandardTwoClickEmailProtection { Return } - $WantedState = $state -eq 'enabled' ? $true : $false + $WantedState = $State -eq 'enabled' ? $true : $false $StateIsCorrect = $CurrentState -eq $WantedState ? $true : $false if ($Settings.remediate -eq $true) { Write-Host 'Time to remediate two-click email protection' if ($StateIsCorrect -eq $true) { - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Two-click email protection is already set to $state." -sev Info + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Two-click email protection is already set to $State." -sev Info } else { try { $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-OrganizationConfig' -cmdParams @{ TwoClickMailPreviewEnabled = $WantedState } -useSystemMailbox $true - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully set two-click email protection to $state." -sev Info + $StateIsCorrect = -not $StateIsCorrect # Toggle the state to reflect the change + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully set two-click email protection to $State." -sev Info } catch { $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to set two-click email protection to $state. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to set two-click email protection to $State. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage } } } if ($Settings.alert -eq $true) { if ($StateIsCorrect -eq $true) { - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Two-click email protection is correctly set to $state." -sev Info + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Two-click email protection is correctly set to $State." -sev Info } else { - Write-StandardsAlert -message "Two-click email protection is not correctly set to $state, but instead $($CurrentState ? 'enabled' : 'disabled')" -object @{TwoClickMailPreviewEnabled = $CurrentState } -tenant $Tenant -standardName 'TwoClickEmailProtection' -standardId $Settings.standardId - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Two-click email protection is not correctly set to $state, but instead $($CurrentState ? 'enabled' : 'disabled')" -sev Info + Write-StandardsAlert -message "Two-click email protection is not correctly set to $State, but instead $($CurrentState ? 'enabled' : 'disabled')" -object @{TwoClickMailPreviewEnabled = $CurrentState } -tenant $Tenant -standardName 'TwoClickEmailProtection' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Two-click email protection is not correctly set to $State, but instead $($CurrentState ? 'enabled' : 'disabled')" -sev Info } } From c02efa9664f3cf967e96633a76ffb5fbb8b90350 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sat, 14 Jun 2025 15:32:21 +0200 Subject: [PATCH 098/160] creation delay --- Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 index 75c4fb04cfa4..c85d8bc4324a 100644 --- a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 @@ -134,6 +134,14 @@ function New-CIPPCAPolicy { if ($location.countriesAndRegions) { $location.countriesAndRegions = @($location.countriesAndRegions) } $Body = ConvertTo-Json -InputObject $Location $GraphRequest = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations' -body $body -Type POST -tenantid $tenantfilter -asApp $true + $retryCount = 0 + do { + Write-Host "Checking for location $($GraphRequest.id) attempt $retryCount. $TenantFilter" + $LocationRequest = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations' -tenantid $tenantfilter -asApp $true | Where-Object -Property id -EQ $GraphRequest.id + Write-Host "LocationRequest: $($LocationRequest.id)" + Start-Sleep -Seconds 1 + $retryCount++ + } while ((!$LocationRequest -or !$LocationRequest.id) -and ($retryCount -lt 5)) Write-LogMessage -Headers $User -API $APINAME -message "Created new Named Location: $($location.displayName)" -Sev 'Info' [pscustomobject]@{ id = $GraphRequest.id @@ -237,7 +245,7 @@ function New-CIPPCAPolicy { Write-Information 'Creating' if ($JSONobj.GrantControls.authenticationStrength.policyType -or $JSONObj.$jsonobj.LocationInfo) { #quick fix for if the policy isn't available - Start-Sleep 1 + Start-Sleep 3 } $null = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/policies' -tenantid $tenantfilter -type POST -body $RawJSON -asApp $true Write-LogMessage -Headers $User -API $APINAME -tenant $($Tenant) -message "Added Conditional Access Policy $($JSONObj.Displayname)" -Sev 'Info' From 17722b710a7db86f121cdc46dcb494a855ddc0cb Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sat, 14 Jun 2025 15:32:47 +0200 Subject: [PATCH 099/160] policy delay --- Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 index c85d8bc4324a..0e4d23694b98 100644 --- a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 @@ -139,7 +139,7 @@ function New-CIPPCAPolicy { Write-Host "Checking for location $($GraphRequest.id) attempt $retryCount. $TenantFilter" $LocationRequest = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations' -tenantid $tenantfilter -asApp $true | Where-Object -Property id -EQ $GraphRequest.id Write-Host "LocationRequest: $($LocationRequest.id)" - Start-Sleep -Seconds 1 + Start-Sleep -Seconds 2 $retryCount++ } while ((!$LocationRequest -or !$LocationRequest.id) -and ($retryCount -lt 5)) Write-LogMessage -Headers $User -API $APINAME -message "Created new Named Location: $($location.displayName)" -Sev 'Info' @@ -244,7 +244,6 @@ function New-CIPPCAPolicy { } else { Write-Information 'Creating' if ($JSONobj.GrantControls.authenticationStrength.policyType -or $JSONObj.$jsonobj.LocationInfo) { - #quick fix for if the policy isn't available Start-Sleep 3 } $null = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/policies' -tenantid $tenantfilter -type POST -body $RawJSON -asApp $true From d425c7c577f347beff4cd8bc458de4101765140a Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sat, 14 Jun 2025 16:48:44 +0200 Subject: [PATCH 100/160] low domain score alert --- .../Alerts/Get-CIPPAlertLowDomainScore.ps1 | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 Modules/CIPPCore/Public/Alerts/Get-CIPPAlertLowDomainScore.ps1 diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertLowDomainScore.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertLowDomainScore.ps1 new file mode 100644 index 000000000000..5e2601a7bc36 --- /dev/null +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertLowDomainScore.ps1 @@ -0,0 +1,25 @@ +function Get-CIPPAlertLowDomainScore { + <# + .FUNCTIONALITY + Entrypoint + #> + [CmdletBinding()] + Param ( + [Parameter(Mandatory)] + $TenantFilter, + [Alias('input')] + [ValidateRange(0, 100)] + [int]$InputValue = 70 + ) + + $DomainData = Get-CIPPDomainAnalyser -TenantFilter $TenantFilter + $LowScoreDomains = $DomainData | Where-Object { + $_.ScorePercentage -lt $InputValue -and $_.ScorePercentage -ne '' + } | ForEach-Object { + "$($_.Domain): Domain security score is $($_.ScorePercentage)%, which is below the threshold of $InputValue%. Issues: $($_.ScoreExplanation)" + } + + if ($LowScoreDomains) { + Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $LowScoreDomains + } +} From 163994dc5979553a5132be68e999eacbce6e0b5a Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sat, 14 Jun 2025 18:19:40 +0200 Subject: [PATCH 101/160] bulk license support --- .../Users/Invoke-ExecBulkLicense.ps1 | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBulkLicense.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBulkLicense.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBulkLicense.ps1 new file mode 100644 index 000000000000..fd81b158e865 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBulkLicense.ps1 @@ -0,0 +1,85 @@ +Function Invoke-ExecBulkLicense { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Identity.User.ReadWrite + #> + [CmdletBinding()] + param ( + $Request, + $TriggerMetadata + ) + + $APIName = $TriggerMetadata.FunctionName + $Results = [System.Collections.Generic.List[string]]::new() + $StatusCode = [HttpStatusCode]::OK + + try { + $UserRequests = $Request.Body + $TenantGroups = $UserRequests | Group-Object -Property tenantFilter + + foreach ($TenantGroup in $TenantGroups) { + $TenantFilter = $TenantGroup.Name + $TenantRequests = $TenantGroup.Group + $AllUsers = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users?&`$select=id,userPrincipalName,assignedLicenses" -tenantid $TenantFilter + $UserLookup = @{} + foreach ($User in $AllUsers) { + $UserLookup[$User.id] = $User + } + + # Process each user request + foreach ($UserRequest in $TenantRequests) { + try { + $UserId = $UserRequest.userIds + $User = $UserLookup[$UserId] + $UserPrincipalName = $User.userPrincipalName + $LicenseOperation = $UserRequest.LicenseOperation + $RemoveAllLicenses = [bool]$UserRequest.RemoveAllLicenses + $Licenses = $UserRequest.Licenses | ForEach-Object { $_.value } + # Handle license operations + if ($LicenseOperation -eq 'Add' -or $LicenseOperation -eq 'Replace') { + $AddLicenses = $Licenses + } + + if ($LicenseOperation -eq 'Remove' -and $RemoveAllLicenses) { + $RemoveLicenses = $User.assignedLicenses.skuId + } elseif ($LicenseOperation -eq 'Remove') { + $RemoveLicenses = $Licenses + } elseif ($LicenseOperation -eq 'Replace') { + $RemoveReplace = $User.assignedLicenses.skuId + if ($RemoveReplace) { Set-CIPPUserLicense -UserId $UserId -TenantFilter $TenantFilter -RemoveLicenses $RemoveReplace } + } elseif ($RemoveAllLicenses) { + $RemoveLicenses = $User.assignedLicenses.skuId + } + #todo: Actually build bulk support into set-cippuserlicense. + $TaskResults = Set-CIPPUserLicense -UserId $UserId -TenantFilter $TenantFilter -AddLicenses $AddLicenses -RemoveLicenses $RemoveLicenses + + $Results.Add($TaskResults) + Write-LogMessage -API $APIName -tenant $TenantFilter -message "Successfully processed licenses for user $UserPrincipalName" -Sev 'Info' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Results.Add("Failed to process licenses for user $($UserRequest.userIds). Error: $($ErrorMessage.NormalizedError)") + Write-LogMessage -API $APIName -tenant $TenantFilter -message "Failed to process licenses for user $($UserRequest.userIds). Error: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage + } + } + } + + $Body = @{ + Results = @($Results) + } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $StatusCode = [HttpStatusCode]::BadRequest + $Body = @{ + Results = @("Failed to process bulk license operation: $($ErrorMessage.NormalizedError)") + } + Write-LogMessage -API $APIName -message "Failed to process bulk license operation: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage + } + + # Return response + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = $Body + }) +} From b1a7451248ff466453ddfffac86f8b5a62c3a0bf Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sat, 14 Jun 2025 19:54:33 +0200 Subject: [PATCH 102/160] teams meeting --- ...e-CIPPStandardTeamsMeetingVerification.ps1 | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMeetingVerification.ps1 diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMeetingVerification.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMeetingVerification.ps1 new file mode 100644 index 000000000000..c7938c496d19 --- /dev/null +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMeetingVerification.ps1 @@ -0,0 +1,77 @@ +function Invoke-CIPPStandardTeamsMeetingVerification { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) TeamsMeetingVerification + .SYNOPSIS + (Label) Meeting Verification (ReCaptcha) for Teams + .DESCRIPTION + (Helptext) Configures the CAPTCHA verification for external users joining Teams meetings. This helps prevent unauthorized AI notetakers and bots from joining meetings. + (DocsDescription) Configures the CAPTCHA verification for external users joining Teams meetings. This helps prevent unauthorized AI notetakers and bots from joining meetings. When enabled, external users from untrusted organizations or anonymous users will need to complete a CAPTCHA verification before joining meetings. + .NOTES + CAT + Teams Standards + TAG + ADDEDCOMPONENT + {"type":"autoComplete","required":true,"multiple":false,"creatable":false,"name":"standards.TeamsMeetingVerification.CaptchaVerificationForMeetingJoin","label":"CAPTCHA verification for meeting join","options":[{"label":"Not Required","value":"NotRequired"},{"label":"Anonymous Users and Untrusted Organizations","value":"AnonymousUsersAndUntrustedOrganizations"}]} + IMPACT + Low Impact + ADDEDDATE + 2025-06-14 + POWERSHELLEQUIVALENT + Set-CsTeamsMeetingPolicy -Identity Global -CaptchaVerificationForMeetingJoin AnonymousUsersAndUntrustedOrganizations + RECOMMENDEDBY + "Microsoft" + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + .LINK + https://docs.cipp.app/user-documentation/tenant/standards/list-standards + .LINK + https://learn.microsoft.com/en-us/microsoftteams/join-verification-check + #> + ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'TeamsMeetingVerification' + + param($Tenant, $Settings) + $CurrentState = New-TeamsRequest -TenantFilter $Tenant -Cmdlet 'Get-CsTeamsMeetingPolicy' -CmdParams @{Identity = 'Global' } | Select-Object CaptchaVerificationForMeetingJoin + $CaptchaVerificationForMeetingJoin = $Settings.CaptchaVerificationForMeetingJoin.value ?? $Settings.CaptchaVerificationForMeetingJoin + $StateIsCorrect = ($CurrentState.CaptchaVerificationForMeetingJoin -eq $CaptchaVerificationForMeetingJoin) + + if ($Settings.remediate -eq $true) { + if ($StateIsCorrect -eq $true) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Teams Meeting Verification Policy already set.' -sev Info + } else { + $cmdParams = @{ + Identity = 'Global' + CaptchaVerificationForMeetingJoin = $CaptchaVerificationForMeetingJoin + } + + try { + New-TeamsRequest -TenantFilter $Tenant -Cmdlet 'Set-CsTeamsMeetingPolicy' -CmdParams $cmdParams + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Updated Teams Meeting Verification Policy' -sev Info + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to set Teams Meeting Verification Policy. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + } + } + } + + if ($Settings.alert -eq $true) { + if ($StateIsCorrect -eq $true) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Teams Meeting Verification Policy is set correctly.' -sev Info + } else { + Write-StandardsAlert -message 'Teams Meeting Verification Policy is not set correctly.' -object $CurrentState -tenant $Tenant -standardName 'TeamsMeetingVerification' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Teams Meeting Verification Policy is not set correctly.' -sev Info + } + } + + if ($Settings.report -eq $true) { + if ($StateIsCorrect) { + $FieldValue = $true + } else { + $FieldValue = $CurrentState + } + Set-CIPPStandardsCompareField -FieldName 'standards.TeamsMeetingVerification' -FieldValue $FieldValue -Tenant $Tenant + Add-CIPPBPAField -FieldName 'TeamsMeetingVerification' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant + } +} From ea55af61c8ad5784afe3a1c24bb72b71837d2ecd Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sat, 14 Jun 2025 22:47:32 +0200 Subject: [PATCH 103/160] New alert --- .../Get-CIPPAlertGlobalAdminNoAltEmail.ps1 | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 Modules/CIPPCore/Public/Alerts/Get-CIPPAlertGlobalAdminNoAltEmail.ps1 diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertGlobalAdminNoAltEmail.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertGlobalAdminNoAltEmail.ps1 new file mode 100644 index 000000000000..3d9805f1fa0a --- /dev/null +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertGlobalAdminNoAltEmail.ps1 @@ -0,0 +1,31 @@ +function Get-CIPPAlertGlobalAdminNoAltEmail { + <# + .FUNCTIONALITY + Entrypoint + #> + [CmdletBinding()] + Param ( + [Parameter(Mandatory = $false)] + [Alias('input')] + $InputValue, + $TenantFilter + ) + try { + # Get all Global Admin accounts using the role template ID + $globalAdmins = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/directoryRoles/roleTemplateId=62e90394-69f5-4237-9190-012177145e10/members?`$select=id,displayName,userPrincipalName,otherMails" -tenantid $($TenantFilter) -AsApp $true | Where-Object { + $_.userDisplayName -ne 'On-Premises Directory Synchronization Service Account' -and $_.'@odata.type' -eq '#microsoft.graph.user' + } + + # Filter for Global Admins without alternate email addresses + $adminsWithoutAltEmail = $globalAdmins | Where-Object { + $null -eq $_.otherMails -or $_.otherMails.Count -eq 0 + } + + if ($adminsWithoutAltEmail.Count -gt 0) { + $AlertData = "The following Global Admin accounts do not have an alternate email address set: $($adminsWithoutAltEmail.userPrincipalName -join ', ')" + Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData + } + } catch { + Write-LogMessage -message "Failed to check alternate email status for Global Admins: $($_.exception.message)" -API 'Global Admin Alt Email Alerts' -tenant $TenantFilter -sev Error + } +} From 979671dc71070688e3d0080f194cfcd4cbc6594f Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Sun, 15 Jun 2025 14:05:52 +0800 Subject: [PATCH 104/160] fix casing and improve forwarding behavior --- .../Invoke-ExecEmailForward.ps1 | 12 ++-- .../Users/Invoke-CIPPOffboardingJob.ps1 | 4 +- .../Users/Invoke-ListUserMailboxDetails.ps1 | 55 ++++++++++++++----- openapi.json | 4 +- 4 files changed, 53 insertions(+), 22 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecEmailForward.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecEmailForward.ps1 index 955606318b02..c9839dd12cf3 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecEmailForward.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecEmailForward.ps1 @@ -12,15 +12,19 @@ Function Invoke-ExecEmailForward { $Tenantfilter = $request.body.tenantfilter $username = $request.body.userid - $ForwardingAddress = $request.body.ForwardInternal.value + if ($request.body.ForwardInternal -is [string]) { + $ForwardingAddress = $request.body.ForwardInternal + } else {($request.body.ForwardInternal.value) + $ForwardingAddress = $request.body.ForwardInternal.value + } $ForwardingSMTPAddress = $request.body.ForwardExternal $ForwardOption = $request.body.forwardOption $APIName = $Request.Params.CIPPEndpoint - [bool]$KeepCopy = if ($request.body.keepCopy -eq 'true') { $true } else { $false } + [bool]$KeepCopy = if ($request.body.KeepCopy -eq 'true') { $true } else { $false } if ($ForwardOption -eq 'internalAddress') { try { - Set-CIPPForwarding -userid $username -tenantFilter $TenantFilter -APIName $APINAME -Headers $Request.Headers -Forward $ForwardingAddress -keepCopy $KeepCopy + Set-CIPPForwarding -userid $username -tenantFilter $TenantFilter -APIName $APINAME -Headers $Request.Headers -Forward $ForwardingAddress -KeepCopy $KeepCopy if (-not $request.body.KeepCopy) { $results = "Forwarding all email for $($username) to $($ForwardingAddress) and not keeping a copy" } else { @@ -35,7 +39,7 @@ Function Invoke-ExecEmailForward { if ($ForwardOption -eq 'ExternalAddress') { try { - Set-CIPPForwarding -userid $username -tenantFilter $TenantFilter -APIName $APINAME -Headers $Request.Headers -forwardingSMTPAddress $ForwardingSMTPAddress -keepCopy $KeepCopy + Set-CIPPForwarding -userid $username -tenantFilter $TenantFilter -APIName $APINAME -Headers $Request.Headers -forwardingSMTPAddress $ForwardingSMTPAddress -KeepCopy $KeepCopy if (-not $request.body.KeepCopy) { $results = "Forwarding all email for $($username) to $($ForwardingSMTPAddress) and not keeping a copy" } else { diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-CIPPOffboardingJob.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-CIPPOffboardingJob.ps1 index 50e985309f1a..7534156ee3ad 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-CIPPOffboardingJob.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-CIPPOffboardingJob.ps1 @@ -45,10 +45,10 @@ function Invoke-CIPPOffboardingJob { Set-CIPPOutOfOffice -tenantFilter $TenantFilter -userid $username -InternalMessage $Options.OOO -ExternalMessage $Options.OOO -Headers $Headers -APIName $APIName -state 'Enabled' } { $_.forward } { - if (!$Options.keepCopy) { + if (!$Options.KeepCopy) { Set-CIPPForwarding -userid $userid -username $username -tenantFilter $TenantFilter -Forward $Options.forward.value -Headers $Headers -APIName $APIName } else { - $KeepCopy = [boolean]$Options.keepCopy + $KeepCopy = [boolean]$Options.KeepCopy Set-CIPPForwarding -userid $userid -username $username -tenantFilter $TenantFilter -Forward $Options.forward.value -KeepCopy $KeepCopy -Headers $Headers -APIName $APIName } } 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 86bb0bc44c5b..1f5ed5a1a1bf 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 @@ -64,7 +64,7 @@ function Invoke-ListUserMailboxDetails { } ) Write-Host $UserID - $usernames = New-GraphGetRequest -tenantid $TenantFilter -uri 'https://graph.microsoft.com/beta/users?$select=id,userPrincipalName&$top=999' + $usernames = New-GraphGetRequest -tenantid $TenantFilter -uri 'https://graph.microsoft.com/beta/users?$select=id,userPrincipalName,displayName,mailNickname&$top=999' $Results = New-ExoBulkRequest -TenantId $TenantFilter -CmdletArray $Requests -returnWithCommand $true -Anchor $username Write-Host "First line of usernames is $($usernames[0] | ConvertTo-Json)" @@ -144,21 +144,48 @@ function Invoke-ListUserMailboxDetails { $ParsedPerms = @() } - # Get forwarding address - $ForwardingAddress = if ($MailboxDetailedRequest.ForwardingAddress) { - try { - (New-GraphGetRequest -TenantId $TenantFilter -Uri "https://graph.microsoft.com/beta/users/$($MailboxDetailedRequest.ForwardingAddress)").UserPrincipalName - } catch { - try { - '{0} ({1})' -f $MailboxDetailedRequest.ForwardingAddress, (($((New-GraphGetRequest -TenantId $TenantFilter -Uri "https://graph.microsoft.com/beta/users?`$filter=displayName eq '$($MailboxDetailedRequest.ForwardingAddress)'") | Select-Object -First 1 -ExpandProperty UserPrincipalName))) - } catch { - $MailboxDetailedRequest.ForwardingAddress + # Get forwarding address - lazy load contacts only if needed + $ForwardingAddress = $null + if ($MailboxDetailedRequest.ForwardingSmtpAddress) { + # External forwarding + $ForwardingAddress = $MailboxDetailedRequest.ForwardingSmtpAddress -replace '^smtp:', '' + } elseif ($MailboxDetailedRequest.ForwardingAddress) { + # Internal forwarding + $rawAddress = $MailboxDetailedRequest.ForwardingAddress + + if ($rawAddress -match '@') { + # Already an email address + $ForwardingAddress = $rawAddress + } else { + # First try users array + $matchedUser = $usernames | Where-Object { + $_.id -eq $rawAddress -or + $_.displayName -eq $rawAddress -or + $_.mailNickname -eq $rawAddress + } + + if ($matchedUser) { + $ForwardingAddress = $matchedUser.userPrincipalName + } else { + # Query for the specific contact only + try { + # Escape single quotes in the filter value + $escapedAddress = $rawAddress -replace "'", "''" + $filterQuery = "displayName eq '$escapedAddress' or mailNickname eq '$escapedAddress'" + $contactUri = "https://graph.microsoft.com/beta/contacts?`$filter=$filterQuery&`$select=displayName,mail,mailNickname" + + $matchedContacts = New-GraphGetRequest -tenantid $TenantFilter -uri $contactUri + + if ($matchedContacts -and $matchedContacts.Count -gt 0) { + $ForwardingAddress = $matchedContacts[0].mail + } else { + $ForwardingAddress = $rawAddress + } + } catch { + $ForwardingAddress = $rawAddress + } } } - } elseif ($MailboxDetailedRequest.ForwardingSmtpAddress -and $MailboxDetailedRequest.ForwardingAddress) { - "$($MailboxDetailedRequest.ForwardingAddress) $($MailboxDetailedRequest.ForwardingSmtpAddress)" - } else { - $MailboxDetailedRequest.ForwardingSmtpAddress } $ProhibitSendQuotaString = $MailboxDetailedRequest.ProhibitSendQuota -split ' ' diff --git a/openapi.json b/openapi.json index 9ac5a92720a0..ffb4023947fe 100644 --- a/openapi.json +++ b/openapi.json @@ -2850,7 +2850,7 @@ "schema": { "type": "string" }, - "name": "keepCopy", + "name": "KeepCopy", "in": "body" } ], @@ -3829,7 +3829,7 @@ "schema": { "type": "string" }, - "name": "keepCopy", + "name": "KeepCopy", "in": "body" }, { From 81c15951c05f5cc7b974d2e99ae3b65c0777d7fc Mon Sep 17 00:00:00 2001 From: Jr7468 Date: Sun, 15 Jun 2025 16:09:17 +0100 Subject: [PATCH 105/160] Created a function to track new risky users and alert on new entries --- .../Alerts/Get-CIPPAlertNewRiskyUsers.ps1 | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 Modules/CIPPCore/Public/Alerts/Get-CIPPAlertNewRiskyUsers.ps1 diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertNewRiskyUsers.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertNewRiskyUsers.ps1 new file mode 100644 index 000000000000..d8252ad12f65 --- /dev/null +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertNewRiskyUsers.ps1 @@ -0,0 +1,77 @@ +function Get-CIPPAlertNewRiskyUsers { + <# + .FUNCTIONALITY + Entrypoint + #> + [CmdletBinding()] + Param ( + [Parameter(Mandatory = $false)] + [Alias('input')] + $TenantFilter + ) + $Deltatable = Get-CIPPTable -Table DeltaCompare + try { + # Check if tenant has P2 capabilities + $Capabilities = Get-CIPPTenantCapabilities -TenantFilter $TenantFilter + if (-not $Capabilities.AADPremiumService) { + Write-AlertMessage -tenant $($TenantFilter) -message 'Tenant does not have Azure AD Premium P2 licensing required for risky users detection' + return + } + + $Filter = "PartitionKey eq 'RiskyUsersDelta' and RowKey eq '{0}'" -f $TenantFilter + $RiskyUsersDelta = (Get-CIPPAzDataTableEntity @Deltatable -Filter $Filter).delta | ConvertFrom-Json -ErrorAction SilentlyContinue + + # Get current risky users with more detailed information + $NewDelta = (New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/identityProtection/riskyUsers' -tenantid $TenantFilter) | Select-Object userPrincipalName, riskLevel, riskState, riskDetail, riskLastUpdatedDateTime, isProcessing, history + + $NewDeltatoSave = $NewDelta | ConvertTo-Json -Depth 10 -Compress -ErrorAction SilentlyContinue | Out-String + $DeltaEntity = @{ + PartitionKey = 'RiskyUsersDelta' + RowKey = [string]$TenantFilter + delta = "$NewDeltatoSave" + } + Add-CIPPAzDataTableEntity @DeltaTable -Entity $DeltaEntity -Force + + if ($RiskyUsersDelta) { + $AlertData = $NewDelta | Where-Object { + $_.userPrincipalName -notin $RiskyUsersDelta.userPrincipalName + } | ForEach-Object { + $riskHistory = if ($_.history) { + $latestHistory = $_.history | Sort-Object -Property riskLastUpdatedDateTime -Descending | Select-Object -First 1 + "Previous Risk Level: $($latestHistory.riskLevel), Last Updated: $($latestHistory.riskLastUpdatedDateTime)" + } + else { + 'No previous risk history' + } + + # Map risk level to severity + $severity = switch ($_.riskLevel) { + 'high' { 'Critical' } + 'medium' { 'Warning' } + 'low' { 'Info' } + default { 'Info' } + } + + @{ + Message = "New risky user detected: $($_.userPrincipalName)" + Details = @{ + RiskLevel = $_.riskLevel + RiskState = $_.riskState + RiskDetail = $_.riskDetail + LastUpdated = $_.riskLastUpdatedDateTime + IsProcessing = $_.isProcessing + RiskHistory = $riskHistory + Severity = $severity + } + } + } + + if ($AlertData) { + Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData + } + } + } + catch { + Write-AlertMessage -tenant $($TenantFilter) -message "Could not get risky users for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" + } +} From 904a9e1ab895c96888691e7398ded0c811f60bb6 Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Mon, 16 Jun 2025 11:11:02 +0800 Subject: [PATCH 106/160] Update alert format to make consistent and fix issue with formatting --- .../Public/Alerts/Get-CIPPAlertAppSecretExpiry.ps1 | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertAppSecretExpiry.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertAppSecretExpiry.ps1 index cef5ba03ba14..ac0a0026ae58 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertAppSecretExpiry.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertAppSecretExpiry.ps1 @@ -18,13 +18,22 @@ function Get-CIPPAlertAppSecretExpiry { return } - $AlertData = foreach ($App in $applist) { + $AlertData = [System.Collections.Generic.List[PSCustomObject]]::new() + + foreach ($App in $applist) { Write-Host "checking $($App.displayName)" if ($App.passwordCredentials) { foreach ($Credential in $App.passwordCredentials) { if ($Credential.endDateTime -lt (Get-Date).AddDays(30) -and $Credential.endDateTime -gt (Get-Date).AddDays(-7)) { Write-Host ("Application '{0}' has secrets expiring on {1}" -f $App.displayName, $Credential.endDateTime) - @{ DisplayName = $App.displayName; Expires = $Credential.endDateTime } + + $Message = [PSCustomObject]@{ + AppName = $App.displayName + AppId = $App.appId + Expires = $Credential.endDateTime + Tenant = $TenantFilter + } + $AlertData.Add($Message) } } } From 3d764d3d49712aee6c4357b0530451d4b2b86cc8 Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Mon, 16 Jun 2025 17:36:24 +0800 Subject: [PATCH 107/160] Make BPA report list sorted --- .../Tenant/Standards/Invoke-ListBPATemplates.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListBPATemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListBPATemplates.ps1 index c32e70d81ea9..356f5443ece3 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListBPATemplates.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListBPATemplates.ps1 @@ -34,7 +34,7 @@ Function Invoke-ListBPATemplates { foreach ($Template in $Templates) { $Template.JSON = $Template.JSON -replace '"parameters":', '"Parameters":' } - $Templates = $Templates.JSON | ConvertFrom-Json + $Templates = $Templates.JSON | ConvertFrom-Json | Sort-Object Name } else { $Templates = $Templates | ForEach-Object { $TemplateJson = $_.JSON -replace '"parameters":', '"Parameters":' @@ -45,7 +45,7 @@ Function Invoke-ListBPATemplates { Name = $Template.Name Style = $Template.Style } - } + } | Sort-Object Name } # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ From 9bb55ca1f701d46fbd4a3a1dd9680ba6b5ab9c98 Mon Sep 17 00:00:00 2001 From: ngms-psh Date: Mon, 16 Jun 2025 18:15:09 +0200 Subject: [PATCH 108/160] Add standard to add DMARC record to MOERA domains --- .../Invoke-CIPPStandardAddDMARCToMOERA.ps1 | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDMARCToMOERA.ps1 diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDMARCToMOERA.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDMARCToMOERA.ps1 new file mode 100644 index 000000000000..ff65bf35ea65 --- /dev/null +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDMARCToMOERA.ps1 @@ -0,0 +1,136 @@ +function Invoke-CIPPStandardAddDMARCToMOERA { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) AddDMARCToMOERA + .SYNOPSIS + (Label) Enables DMARC on MOERA (onmicrosoft.com) domains + .DESCRIPTION + (Helptext) This should be enabled even if the MOERA (onmicrosoft.com) domains is not used for sending. Enabling this prevents email spoofing. The default value is 'v=DMARC1; p=reject;' recommended because the domain is only used within M365 and reporting is not needed. Omitting pct tag default to 100% + (DocsDescription) Adds a DMARC record to MOERA (onmicrosoft.com) domains. This should be enabled even if the MOERA (onmicrosoft.com) domains is not used for sending. Enabling this prevents email spoofing. The default record is 'v=DMARC1; p=reject;' recommended because the domain is only used within M365 and reporting is not needed. Omitting pct tag default to 100% + .NOTES + CAT + Global Standards + TAG + "CIS" + "Security" + "PhishingProtection" + ADDEDCOMPONENT + {"type":"autoComplete","multiple":false,"creatable":true,"required":false,"placeholder":"v=DMARC1; p=reject; (recommended)","label":"Value","name":"standards.AddDMARCToMOERA.RecordValue","options":[{"label":"v=DMARC1; p=reject; (recommended)","value":"v=DMARC1; p=reject;"}]} + IMPACT + Low Impact + ADDEDDATE + 2025-06-16 + POWERSHELLEQUIVALENT + Portal only + RECOMMENDEDBY + "CIS" + "Microsoft" + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + .LINK + https://docs.cipp.app/user-documentation/tenant/standards/list-standards + #> + + param($Tenant, $Settings) + #$Rerun -Type Standard -Tenant $Tenant -API 'AddDMARCToMOERA' -Settings $Settings + + $RecordModel = [PSCustomObject]@{ + HostName = '_dmarc' + TtlValue = 3600 + Type = 'TXT' + Value = $Settings.RecordValue.Value ?? "v=DMARC1; p=reject;" + } + + # Get all fallback domains (onmicrosoft.com domains) and check if the DMARC record is set correctly + try { + $Domains = New-GraphGetRequest -scope 'https://admin.microsoft.com/.default' -TenantID $Tenant -Uri 'https://admin.microsoft.com/admin/api/Domains/List' | Where-Object -Property IsInitial -eq $true + + $CurrentInfo = $domains | ForEach-Object { + # Get current DNS records that matches _dmarc hostname and TXT type + $CurrentRecords = New-GraphGetRequest -scope 'https://admin.microsoft.com/.default' -TenantID $Tenant -Uri "https://admin.microsoft.com/admin/api/Domains/Records?domainName=$($_.Name)" | Select-Object -ExpandProperty DnsRecords | Where-Object { $_.HostName -eq $RecordModel.HostName -and $_.Type -eq $RecordModel.Type } + + if ($CurrentRecords.count -eq 0) { + #record not found, return a model with Match set to false + [PSCustomObject]@{ + DomainName = $_.Name + Match = $false + CurrentRecord = $null + } + } else { + foreach ($CurrentRecord in $CurrentRecords) { + # Create variable matching the RecordModel used for comparison + $CurrentRecordModel = [PSCustomObject]@{ + HostName = $CurrentRecord.HostName + TtlValue = $CurrentRecord.TtlValue + Type = $CurrentRecord.Type + Value = $CurrentRecord.Value + } + + # Compare the current record with the expected record model + if (!(Compare-Object -ReferenceObject $RecordModel -DifferenceObject $CurrentRecordModel -Property HostName, TtlValue, Type, Value)) { + [PSCustomObject]@{ + DomainName = $_.Name + Match = $true + CurrentRecord = $CurrentRecord + } + } else { + [PSCustomObject]@{ + DomainName = $_.Name + Match = $false + CurrentRecord = $CurrentRecord + } + } + } + } + } + # Check if match is true and there is only one DMARC record for the domain + $StateIsCorrect = $false -notin $CurrentInfo.Match -and $CurrentInfo.Count -eq 1 + } catch { + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to get dns records for MOERA domains: $(Get-NormalizedError -message $_.Exception.message)" -sev Error + throw "Failed to get dns records for MOERA domains: $(Get-NormalizedError -message $_.Exception.message)" + } + + If ($Settings.remediate -eq $true) { + if ($StateIsCorrect -eq $true) { + Write-LogMessage -API 'Standards' -tenant $tenant -message 'DMARC record is already set for all MOERA (onmicrosoft.com) domains.' -sev Info + } + else { + # Loop through each domain and set the DMARC record, existing misconfigured records and duplicates will be deleted + foreach ($Domain in ($CurrentInfo | Sort-Object -Property DomainName -Unique)) { + try { + foreach ($Record in ($CurrentInfo | Where-Object -Property DomainName -eq $Domain.DomainName)) { + if ($Record.CurrentRecord) { + New-GraphPOSTRequest -tenantid $tenant -scope 'https://admin.microsoft.com/.default' -Uri "https://admin.microsoft.com/admin/api/Domains/Record?domainName=$($Domain.DomainName)" -Body ($Record.CurrentRecord | ConvertTo-Json -Compress) -AddedHeaders @{'x-http-method-override' = 'Delete'} + Write-LogMessage -API 'Standards' -tenant $tenant -message "Deleted incorrect DMARC record for domain $($Domain.DomainName)" -sev Info + } + New-GraphPOSTRequest -tenantid $tenant -scope 'https://admin.microsoft.com/.default' -type "PUT" -Uri "https://admin.microsoft.com/admin/api/Domains/Record?domainName=$($Domain.DomainName)" -Body (@{RecordModel = $RecordModel} | ConvertTo-Json -Compress) + Write-LogMessage -API 'Standards' -tenant $tenant -message "Set DMARC record for domain $($Domain.DomainName)" -sev Info + } + } + catch { + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to set DMARC record for domain $($Domain.DomainName): $(Get-NormalizedError -message $_.Exception.message)" -sev Error + } + } + } + } + + if ($Settings.alert -eq $true) { + if ($StateIsCorrect -eq $true) { + Write-LogMessage -API 'Standards' -tenant $tenant -message 'DMARC record is already set for all MOERA (onmicrosoft.com) domains.' -sev Info + } else { + $UniqueDomains = ($CurrentInfo | Sort-Object -Property DomainName -Unique) + $NotSetDomains = @($UniqueDomains | ForEach-Object {if ($_.Match -eq $false -or ($CurrentInfo | Where-Object -Property DomainName -eq $_.DomainName).Count -eq 1) { $_.DomainName } }) + $Message = "DMARC record is not set for $($NotSetDomains.count) of $($UniqueDomains.count) MOERA (onmicrosoft.com) domains." + + Write-StandardsAlert -message $Message -object @{MissingDMARC = ($NotSetDomains -join ', ')} -tenant $tenant -standardName 'AddDMARCToMOERA' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $tenant -message "$Message. Missing for: $($NotSetDomains -join ', ')" -sev Info + } + } + + if ($Settings.report -eq $true) { + set-CIPPStandardsCompareField -FieldName 'standards.AddDMARCToMOERA' -FieldValue $StateIsCorrect -TenantFilter $Tenant + Add-CIPPBPAField -FieldName 'AddDMARCToMOERA' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant + } +} From a69ebab3bc87003e272694cd31583c5bb044fa3b Mon Sep 17 00:00:00 2001 From: ngms-psh Date: Mon, 16 Jun 2025 18:17:34 +0200 Subject: [PATCH 109/160] Add "DMARC", "MOERA", "onmicrosoft.com" to custom words --- cspell.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cspell.json b/cspell.json index 74367cb934af..3d3fd4853c5c 100644 --- a/cspell.json +++ b/cspell.json @@ -39,7 +39,8 @@ "TNEF", "weburl", "winmail", - "Yubikey" + "Yubikey", + "DMARC" ], "ignoreWords": [ "ACOM", @@ -93,7 +94,9 @@ "exo_outlookaddins", "exo_mailboxaudit", "exo_mailtipsenabled", - "mip_search_auditlog" + "mip_search_auditlog", + "onmicrosoft", + "MOERA" ], "import": [] } From db2e67874f8a7ff2261977fc25966db78741e1fd Mon Sep 17 00:00:00 2001 From: ngms-psh Date: Mon, 16 Jun 2025 21:58:30 +0200 Subject: [PATCH 110/160] Alert for missing 'Domain Name Administrator' gdap role --- .../Invoke-CIPPStandardAddDMARCToMOERA.ps1 | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDMARCToMOERA.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDMARCToMOERA.ps1 index ff65bf35ea65..41ca6358a70b 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDMARCToMOERA.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDMARCToMOERA.ps1 @@ -45,9 +45,9 @@ function Invoke-CIPPStandardAddDMARCToMOERA { # Get all fallback domains (onmicrosoft.com domains) and check if the DMARC record is set correctly try { - $Domains = New-GraphGetRequest -scope 'https://admin.microsoft.com/.default' -TenantID $Tenant -Uri 'https://admin.microsoft.com/admin/api/Domains/List' | Where-Object -Property IsInitial -eq $true + $Domains = New-GraphGetRequest -scope 'https://admin.microsoft.com/.default' -TenantID $Tenant -Uri 'https://admin.microsoft.com/admin/api/Domains/List' | Where-Object -Property Name -like "*.onmicrosoft.com" - $CurrentInfo = $domains | ForEach-Object { + $CurrentInfo = $Domains | ForEach-Object { # Get current DNS records that matches _dmarc hostname and TXT type $CurrentRecords = New-GraphGetRequest -scope 'https://admin.microsoft.com/.default' -TenantID $Tenant -Uri "https://admin.microsoft.com/admin/api/Domains/Records?domainName=$($_.Name)" | Select-Object -ExpandProperty DnsRecords | Where-Object { $_.HostName -eq $RecordModel.HostName -and $_.Type -eq $RecordModel.Type } @@ -88,8 +88,14 @@ function Invoke-CIPPStandardAddDMARCToMOERA { # Check if match is true and there is only one DMARC record for the domain $StateIsCorrect = $false -notin $CurrentInfo.Match -and $CurrentInfo.Count -eq 1 } catch { - Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to get dns records for MOERA domains: $(Get-NormalizedError -message $_.Exception.message)" -sev Error - throw "Failed to get dns records for MOERA domains: $(Get-NormalizedError -message $_.Exception.message)" + if ($_.Exception.Message -like '*403*') { + $Message = "AddDMARCToMOERA: Insufficient permissions. Please ensure the tenant GDAP relationship includes the 'Domain Name Administrator' role: $(Get-NormalizedError -message $_.Exception.message)" + } + else { + $Message = "Failed to get dns records for MOERA domains: $(Get-NormalizedError -message $_.Exception.message)" + } + Write-LogMessage -API 'Standards' -tenant $tenant -message $Message -sev Error + throw $Message } If ($Settings.remediate -eq $true) { From b05704cfb29f507618b7286aa3c7a3dee36c4b13 Mon Sep 17 00:00:00 2001 From: ngms-psh Date: Mon, 16 Jun 2025 22:06:00 +0200 Subject: [PATCH 111/160] Chore: Update comments --- .../Public/Standards/Invoke-CIPPStandardAddDMARCToMOERA.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDMARCToMOERA.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDMARCToMOERA.ps1 index 41ca6358a70b..85e5ba190ec8 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDMARCToMOERA.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDMARCToMOERA.ps1 @@ -7,8 +7,8 @@ function Invoke-CIPPStandardAddDMARCToMOERA { .SYNOPSIS (Label) Enables DMARC on MOERA (onmicrosoft.com) domains .DESCRIPTION - (Helptext) This should be enabled even if the MOERA (onmicrosoft.com) domains is not used for sending. Enabling this prevents email spoofing. The default value is 'v=DMARC1; p=reject;' recommended because the domain is only used within M365 and reporting is not needed. Omitting pct tag default to 100% - (DocsDescription) Adds a DMARC record to MOERA (onmicrosoft.com) domains. This should be enabled even if the MOERA (onmicrosoft.com) domains is not used for sending. Enabling this prevents email spoofing. The default record is 'v=DMARC1; p=reject;' recommended because the domain is only used within M365 and reporting is not needed. Omitting pct tag default to 100% + (Helptext) Note: requires 'Domain Name Administrator' GDAP role. This should be enabled even if the MOERA (onmicrosoft.com) domains is not used for sending. Enabling this prevents email spoofing. The default value is 'v=DMARC1; p=reject;' recommended because the domain is only used within M365 and reporting is not needed. Omitting pct tag default to 100% + (DocsDescription) Note: requires 'Domain Name Administrator' GDAP role. Adds a DMARC record to MOERA (onmicrosoft.com) domains. This should be enabled even if the MOERA (onmicrosoft.com) domains is not used for sending. Enabling this prevents email spoofing. The default record is 'v=DMARC1; p=reject;' recommended because the domain is only used within M365 and reporting is not needed. Omitting pct tag default to 100% .NOTES CAT Global Standards From a82676d65b954e8c30d73c38bfff3a5f58cf4dd5 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 16 Jun 2025 16:14:36 -0400 Subject: [PATCH 112/160] port manual credential option to ExecCombinedSetup --- .../CIPP/Setup/Invoke-ExecCombinedSetup.ps1 | 51 ++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecCombinedSetup.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecCombinedSetup.ps1 index 6331d1e2fdc9..3fa7ac8df368 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecCombinedSetup.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecCombinedSetup.ps1 @@ -1,17 +1,25 @@ using namespace System.Net -Function Invoke-ExecCombinedSetup { +function Invoke-ExecCombinedSetup { <# .FUNCTIONALITY Entrypoint,AnyTenant .ROLE CIPP.AppSettings.ReadWrite #> + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] [CmdletBinding()] param($Request, $TriggerMetadata) #Make arraylist of Results $Results = [System.Collections.ArrayList]::new() try { + # Set up Azure context if needed for Key Vault access + if ($env:AzureWebJobsStorage -ne 'UseDevelopmentStorage=true' -and $env:MSI_SECRET) { + Disable-AzContextAutosave -Scope Process | Out-Null + $null = Connect-AzAccount -Identity + $SubscriptionId = $env:WEBSITE_OWNER_NAME -split '\+' | Select-Object -First 1 + $null = Set-AzContext -SubscriptionId $SubscriptionId + } if ($request.body.selectedBaselines -and $request.body.baselineOption -eq 'downloadBaselines') { #do a single download of the selected baselines. foreach ($template in $request.body.selectedBaselines) { @@ -56,6 +64,47 @@ Function Invoke-ExecCombinedSetup { $notificationResults = Set-CIPPNotificationConfig @notificationConfig $Results.add($notificationResults) } + if ($Request.Body.selectedOption -eq 'Manual') { + $KV = $env:WEBSITE_DEPLOYMENT_ID + + if ($env:AzureWebJobsStorage -eq 'UseDevelopmentStorage=true') { + $DevSecretsTable = Get-CIPPTable -tablename 'DevSecrets' + $Secret = Get-CIPPAzDataTableEntity @DevSecretsTable -Filter "PartitionKey eq 'Secret' and RowKey eq 'Secret'" + if (!$Secret) { + $Secret = [PSCustomObject]@{ + 'PartitionKey' = 'Secret' + 'RowKey' = 'Secret' + 'TenantId' = '' + 'RefreshToken' = '' + 'ApplicationId' = '' + 'ApplicationSecret' = '' + } + Add-CIPPAzDataTableEntity @DevSecretsTable -Entity $Secret -Force + } + + if ($Request.Body.tenantId) { $Secret.TenantId = $Request.Body.tenantid } + if ($Request.Body.applicationId) { $Secret.ApplicationId = $Request.Body.applicationId } + if ($Request.Body.ApplicationSecret) { $Secret.ApplicationSecret = $Request.Body.ApplicationSecret } + Add-CIPPAzDataTableEntity @DevSecretsTable -Entity $Secret -Force + $Results.add('Manual credentials have been set in the DevSecrets table.') + } else { + if ($Request.Body.tenantId) { + Set-AzKeyVaultSecret -VaultName $kv -Name 'tenantid' -SecretValue (ConvertTo-SecureString -String $Request.Body.tenantId -AsPlainText -Force) + $Results.add('Set tenant ID in Key Vault.') + } + if ($Request.Body.applicationId) { + Set-AzKeyVaultSecret -VaultName $kv -Name 'applicationid' -SecretValue (ConvertTo-SecureString -String $Request.Body.applicationId -AsPlainText -Force) + $Results.add('Set application ID in Key Vault.') + } + if ($Request.Body.applicationSecret) { + Set-AzKeyVaultSecret -VaultName $kv -Name 'applicationsecret' -SecretValue (ConvertTo-SecureString -String $Request.Body.applicationSecret -AsPlainText -Force) + $Results.add('Set application secret in Key Vault.') + } + } + + $Results.add('Manual credentials setup has been completed.') + } + $Results.add('Setup is now complete. You may navigate away from this page and start using CIPP.') #one more force of reauth so env vars update. $auth = Get-CIPPAuthentication From e556a1fbd3cfd57795e5439ab9410a4014827981 Mon Sep 17 00:00:00 2001 From: ngms-psh Date: Mon, 16 Jun 2025 23:12:21 +0200 Subject: [PATCH 113/160] Alert for missing GDAP Role, "enabled" the standard --- ...voke-CIPPStandardDisableSelfServiceLicenses.ps1 | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 index 62a20519f44c..fa64a4427308 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 @@ -31,15 +31,17 @@ function Invoke-CIPPStandardDisableSelfServiceLicenses { param($Tenant, $Settings) ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'DisableSelfServiceLicenses' - # disable for now - MS enforced role requirement - Write-LogMessage -API 'Standards' -tenant $tenant -message 'Self Service Licenses cannot be disabled' -sev Error - return - try { $selfServiceItems = (New-GraphGETRequest -scope 'aeb86249-8ea3-49e2-900b-54cc8e308f85/.default' -uri 'https://licensing.m365.microsoft.com/v1.0/policies/AllowSelfServicePurchase/products' -tenantid $Tenant).items } catch { - Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to retrieve self service products: $($_.Exception.Message)" -sev Error - throw "Failed to retrieve self service products: $($_.Exception.Message)" + if ($_.Exception.Message -like '*403*') { + $Message = "Failed to retrieve self service products: Insufficient permissions. Please ensure the tenant GDAP relationship includes the 'Billing Administrator' role: $($_.Exception.Message)" + } + else { + $Message = "Failed to retrieve self service products: $($_.Exception.Message)" + } + Write-LogMessage -API 'Standards' -tenant $tenant -message $Message -sev Error + throw $Message } if ($settings.remediate) { From 7aafce29c0c7f04b158412c3ff07e8de8211a24d Mon Sep 17 00:00:00 2001 From: ngms-psh Date: Mon, 16 Jun 2025 23:26:02 +0200 Subject: [PATCH 114/160] Chore: Update comments --- .../Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 index fa64a4427308..535aeca021cc 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 @@ -7,8 +7,8 @@ function Invoke-CIPPStandardDisableSelfServiceLicenses { .SYNOPSIS (Label) Disable Self Service Licensing .DESCRIPTION - (Helptext) This standard disables all self service licenses and enables all exclusions - (DocsDescription) This standard disables all self service licenses and enables all exclusions + (Helptext) Note: requires 'Billing Administrator' GDAP role. This standard disables all self service licenses and enables all exclusions + (DocsDescription) Note: requires 'Billing Administrator' GDAP role. This standard disables all self service licenses and enables all exclusions .NOTES CAT Entra (AAD) Standards From b78159b26f2afeb1885fdaddcac867b9d4051adc Mon Sep 17 00:00:00 2001 From: Luke Steward <87503131+sfaxluke@users.noreply.github.com> Date: Tue, 17 Jun 2025 09:20:43 +0100 Subject: [PATCH 115/160] Excluded 8x8 and Gamma domains --- .../Domain Analyser/Push-DomainAnalyserTenant.ps1 | 2 ++ .../CIPPCore/Public/Standards/Invoke-CIPPStandardAddDKIM.ps1 | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-DomainAnalyserTenant.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-DomainAnalyserTenant.ps1 index 34ed6784f272..96b434147f30 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-DomainAnalyserTenant.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-DomainAnalyserTenant.ps1 @@ -30,6 +30,8 @@ function Push-DomainAnalyserTenant { '*.signature365.net' '*.myteamsconnect.io' '*.teams.dstny.com' + '*.msteams.8x8.com' + '*.ucconnect.co.uk' ) $Domains = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/domains' -tenantid $Tenant.customerId | Where-Object { $_.isVerified -eq $true } | ForEach-Object { $Domain = $_ diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDKIM.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDKIM.ps1 index 6a885ec4b92d..8e119023f392 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDKIM.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDKIM.ps1 @@ -76,6 +76,8 @@ function Invoke-CIPPStandardAddDKIM { '*.signature365.net' '*.myteamsconnect.io' '*.teams.dstny.com' + '*.msteams.8x8.com' + '*.ucconnect.co.uk' ) $AllDomains = ($BatchResults | Where-Object { $_.DomainName }).DomainName | ForEach-Object { From c7591bae9d64154516ffbe1aa84564a29a58a8de Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 17 Jun 2025 19:13:56 +0800 Subject: [PATCH 116/160] Fix null response from EXO and remove += --- ...oke-CIPPStandardMailboxRecipientLimits.ps1 | 158 +++++++++++++----- 1 file changed, 116 insertions(+), 42 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1 index 808774259f0f..6f57d05f687c 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1 @@ -61,57 +61,69 @@ function Invoke-CIPPStandardMailboxRecipientLimits { $Mailboxes = New-ExoBulkRequest -tenantid $Tenant -cmdletArray $Requests - # Process mailboxes and categorize them based on their plan limits - $MailboxResults = $Mailboxes | ForEach-Object { - $Mailbox = $_ - $Plan = $MailboxPlanLookup[$Mailbox.MailboxPlanId] - - if ($Plan) { - $PlanMaxRecipients = $Plan.MaxRecipientsPerMessage - - # If mailbox has "Unlimited" set but has a plan, use the plan's limit as the current limit - $CurrentLimit = if ($Mailbox.RecipientLimits -eq 'Unlimited') { - $PlanMaxRecipients - } - else { - $Mailbox.RecipientLimits + # Skip processing entirely if no mailboxes returned - most performant approach + $MailboxResults = @() + $MailboxesToUpdate = @() + $MailboxesWithPlanIssues = @() + + if ($null -ne $Mailboxes -and @($Mailboxes).Count -gt 0) { + # Process mailboxes and categorize them based on their plan limits + $MailboxResults = @($Mailboxes) | ForEach-Object { + $Mailbox = $_ + + # Safe hashtable lookup - check if MailboxPlanId exists and is not null + $Plan = $null + if ($Mailbox.MailboxPlanId -and $MailboxPlanLookup.ContainsKey($Mailbox.MailboxPlanId)) { + $Plan = $MailboxPlanLookup[$Mailbox.MailboxPlanId] } - - if ($Settings.RecipientLimit -gt $PlanMaxRecipients) { - [PSCustomObject]@{ - Type = 'PlanIssue' - Mailbox = $Mailbox - CurrentLimit = $CurrentLimit - PlanLimit = $PlanMaxRecipients - PlanName = $Plan.DisplayName + + if ($Plan) { + $PlanMaxRecipients = $Plan.MaxRecipientsPerMessage + + # If mailbox has "Unlimited" set but has a plan, use the plan's limit as the current limit + $CurrentLimit = if ($Mailbox.RecipientLimits -eq 'Unlimited') { + $PlanMaxRecipients + } + else { + $Mailbox.RecipientLimits + } + + if ($Settings.RecipientLimit -gt $PlanMaxRecipients) { + [PSCustomObject]@{ + Type = 'PlanIssue' + Mailbox = $Mailbox + CurrentLimit = $CurrentLimit + PlanLimit = $PlanMaxRecipients + PlanName = $Plan.DisplayName + } + } + elseif ($CurrentLimit -ne $Settings.RecipientLimit) { + [PSCustomObject]@{ + Type = 'ToUpdate' + Mailbox = $Mailbox + } } } - elseif ($CurrentLimit -ne $Settings.RecipientLimit) { + elseif ($Mailbox.RecipientLimits -ne $Settings.RecipientLimit) { [PSCustomObject]@{ Type = 'ToUpdate' Mailbox = $Mailbox } } } - elseif ($Mailbox.RecipientLimits -ne $Settings.RecipientLimit) { + + # Separate mailboxes into their respective categories only if we have results + $MailboxesToUpdate = $MailboxResults | Where-Object { $_.Type -eq 'ToUpdate' } | Select-Object -ExpandProperty Mailbox + $MailboxesWithPlanIssues = $MailboxResults | Where-Object { $_.Type -eq 'PlanIssue' } | ForEach-Object { [PSCustomObject]@{ - Type = 'ToUpdate' - Mailbox = $Mailbox + Identity = $_.Mailbox.Identity + CurrentLimit = $_.CurrentLimit + PlanLimit = $_.PlanLimit + PlanName = $_.PlanName } } } - # Separate mailboxes into their respective categories - $MailboxesToUpdate = $MailboxResults | Where-Object { $_.Type -eq 'ToUpdate' } | Select-Object -ExpandProperty Mailbox - $MailboxesWithPlanIssues = $MailboxResults | Where-Object { $_.Type -eq 'PlanIssue' } | ForEach-Object { - [PSCustomObject]@{ - Identity = $_.Mailbox.Identity - CurrentLimit = $_.CurrentLimit - PlanLimit = $_.PlanLimit - PlanName = $_.PlanName - } - } - # Remediation if ($Settings.remediate -eq $true) { if ($MailboxesWithPlanIssues.Count -gt 0) { @@ -123,6 +135,20 @@ function Invoke-CIPPStandardMailboxRecipientLimits { if ($MailboxesToUpdate.Count -gt 0) { try { + # Create detailed log data for audit trail + $MailboxChanges = $MailboxesToUpdate | ForEach-Object { + $CurrentLimit = if ($_.RecipientLimits -eq 'Unlimited') { 'Unlimited' } else { $_.RecipientLimits } + @{ + Identity = $_.Identity + DisplayName = $_.DisplayName + PrimarySmtpAddress = $_.PrimarySmtpAddress + CurrentLimit = $CurrentLimit + NewLimit = $Settings.RecipientLimit + } + } + + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Updating recipient limits to $($Settings.RecipientLimit) for $($MailboxesToUpdate.Count) mailboxes" -sev Info -LogData $MailboxChanges + # Create batch requests for mailbox updates $UpdateRequests = $MailboxesToUpdate | ForEach-Object { @{ @@ -138,7 +164,7 @@ function Invoke-CIPPStandardMailboxRecipientLimits { # Execute batch update $null = New-ExoBulkRequest -tenantid $Tenant -cmdletArray $UpdateRequests - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully set recipient limits to $($Settings.RecipientLimit) for $($MailboxesToUpdate.Count) mailboxes" -sev Info + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully applied recipient limits to $($MailboxesToUpdate.Count) mailboxes" -sev Info } catch { $ErrorMessage = Get-CippException -Exception $_ @@ -156,12 +182,60 @@ function Invoke-CIPPStandardMailboxRecipientLimits { Write-LogMessage -API 'Standards' -tenant $Tenant -message "All mailboxes have the correct recipient limit of $($Settings.RecipientLimit)" -sev Info } else { - $AlertMessage = "Found $($MailboxesToUpdate.Count) mailboxes with incorrect recipient limits" + # Create structured alert data + $AlertData = @{ + RequestedLimit = $Settings.RecipientLimit + MailboxesToUpdate = @() + MailboxesWithPlanIssues = @() + } + + # Use Generic List for efficient object collection + $AlertObjects = [System.Collections.Generic.List[Object]]::new() + + # Add mailboxes that need updating + if ($MailboxesToUpdate.Count -gt 0) { + $AlertData.MailboxesToUpdate = $MailboxesToUpdate | ForEach-Object { + $CurrentLimit = if ($_.RecipientLimits -eq 'Unlimited') { 'Unlimited' } else { $_.RecipientLimits } + @{ + Identity = $_.Identity + DisplayName = $_.DisplayName + PrimarySmtpAddress = $_.PrimarySmtpAddress + CurrentLimit = $CurrentLimit + RequiredLimit = $Settings.RecipientLimit + } + } + # Add to alert objects list efficiently + foreach ($Mailbox in $MailboxesToUpdate) { + $AlertObjects.Add($Mailbox) + } + } + + # Add mailboxes with plan issues if ($MailboxesWithPlanIssues.Count -gt 0) { - $AlertMessage += " and $($MailboxesWithPlanIssues.Count) mailboxes where the requested limit exceeds their mailbox plan limit" + $AlertData.MailboxesWithPlanIssues = $MailboxesWithPlanIssues | ForEach-Object { + @{ + Identity = $_.Identity + CurrentLimit = $_.CurrentLimit + PlanLimit = $_.PlanLimit + PlanName = $_.PlanName + RequestedLimit = $Settings.RecipientLimit + } + } + # Add to alert objects list efficiently + foreach ($Mailbox in $MailboxesWithPlanIssues) { + $AlertObjects.Add($Mailbox) + } + } + + # Build alert message efficiently + $AlertMessage = if ($MailboxesWithPlanIssues.Count -gt 0) { + "Found $($MailboxesToUpdate.Count) mailboxes with incorrect recipient limits and $($MailboxesWithPlanIssues.Count) mailboxes where the requested limit exceeds their mailbox plan limit" + } else { + "Found $($MailboxesToUpdate.Count) mailboxes with incorrect recipient limits" } - Write-StandardsAlert -message $AlertMessage -object ($MailboxesToUpdate + $MailboxesWithPlanIssues) -tenant $Tenant -standardName 'MailboxRecipientLimits' -standardId $Settings.standardId - Write-LogMessage -API 'Standards' -tenant $Tenant -message $AlertMessage -sev Info + + Write-StandardsAlert -message $AlertMessage -object $AlertObjects.ToArray() -tenant $Tenant -standardName 'MailboxRecipientLimits' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $Tenant -message $AlertMessage -sev Info -LogData $AlertData } } From 46a49a86e392965df445032ead887b6549a4a3a7 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 17 Jun 2025 13:13:21 -0400 Subject: [PATCH 117/160] fix duplicate autopilot profile resolves ticket 24828141132 --- .../Invoke-CIPPStandardAutopilotProfile.ps1 | 58 ++++++++++--------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotProfile.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotProfile.ps1 index 549c0e866a9d..041628368288 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotProfile.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotProfile.ps1 @@ -43,42 +43,44 @@ function Invoke-CIPPStandardAutopilotProfile { # Get the current configuration try { + # Replace variables in displayname to prevent duplicates + $DisplayName = Get-CIPPTextReplacement -Text $Settings.DisplayName -TenantFilter $Tenant + $CurrentConfig = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles' -tenantid $Tenant | - Where-Object { $_.displayName -eq $Settings.DisplayName } | - Select-Object -Property displayName, description, deviceNameTemplate, language, enableWhiteGlove, extractHardwareHash, outOfBoxExperienceSetting, preprovisioningAllowed + Where-Object { $_.displayName -eq $DisplayName } | + Select-Object -Property displayName, description, deviceNameTemplate, language, enableWhiteGlove, extractHardwareHash, outOfBoxExperienceSetting, preprovisioningAllowed if ($Settings.NotLocalAdmin -eq $true) { $userType = 'Standard' } else { $userType = 'Administrator' } if ($Settings.SelfDeployingMode -eq $true) { $DeploymentMode = 'shared' } else { $DeploymentMode = 'singleUser' } - if ($Settings.AllowWhiteGlove -eq $true) {$Settings.HideChangeAccount = $true} + if ($Settings.AllowWhiteGlove -eq $true) { $Settings.HideChangeAccount = $true } - $StateIsCorrect = ($CurrentConfig.displayName -eq $Settings.DisplayName) -and - ($CurrentConfig.description -eq $Settings.Description) -and - ($CurrentConfig.deviceNameTemplate -eq $Settings.DeviceNameTemplate) -and - ([string]::IsNullOrWhiteSpace($CurrentConfig.language) -and [string]::IsNullOrWhiteSpace($Settings.Languages.value) -or $CurrentConfig.language -eq $Settings.Languages.value) -and - ($CurrentConfig.enableWhiteGlove -eq $Settings.AllowWhiteGlove) -and - ($CurrentConfig.extractHardwareHash -eq $Settings.CollectHash) -and - ($CurrentConfig.outOfBoxExperienceSetting.deviceUsageType -eq $DeploymentMode) -and - ($CurrentConfig.outOfBoxExperienceSetting.escapeLinkHidden -eq $Settings.HideChangeAccount) -and - ($CurrentConfig.outOfBoxExperienceSetting.privacySettingsHidden -eq $Settings.HidePrivacy) -and - ($CurrentConfig.outOfBoxExperienceSetting.eulaHidden -eq $Settings.HideTerms) -and - ($CurrentConfig.outOfBoxExperienceSetting.userType -eq $userType) -and - ($CurrentConfig.outOfBoxExperienceSetting.keyboardSelectionPageSkipped -eq $Settings.AutoKeyboard) - } - catch { + $StateIsCorrect = ($CurrentConfig.displayName -eq $DisplayName) -and + ($CurrentConfig.description -eq $Settings.Description) -and + ($CurrentConfig.deviceNameTemplate -eq $Settings.DeviceNameTemplate) -and + ([string]::IsNullOrWhiteSpace($CurrentConfig.language) -and [string]::IsNullOrWhiteSpace($Settings.Languages.value) -or $CurrentConfig.language -eq $Settings.Languages.value) -and + ($CurrentConfig.enableWhiteGlove -eq $Settings.AllowWhiteGlove) -and + ($CurrentConfig.extractHardwareHash -eq $Settings.CollectHash) -and + ($CurrentConfig.outOfBoxExperienceSetting.deviceUsageType -eq $DeploymentMode) -and + ($CurrentConfig.outOfBoxExperienceSetting.escapeLinkHidden -eq $Settings.HideChangeAccount) -and + ($CurrentConfig.outOfBoxExperienceSetting.privacySettingsHidden -eq $Settings.HidePrivacy) -and + ($CurrentConfig.outOfBoxExperienceSetting.eulaHidden -eq $Settings.HideTerms) -and + ($CurrentConfig.outOfBoxExperienceSetting.userType -eq $userType) -and + ($CurrentConfig.outOfBoxExperienceSetting.keyboardSelectionPageSkipped -eq $Settings.AutoKeyboard) + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to check Autopilot profile: $ErrorMessage" -sev Error $StateIsCorrect = $false } # Remediate if the state is not correct - If ($Settings.remediate -eq $true) { + if ($Settings.remediate -eq $true) { if ($StateIsCorrect -eq $true) { - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Autopilot profile '$($Settings.DisplayName)' already exists" -sev Info + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Autopilot profile '$($DisplayName)' already exists" -sev Info } else { try { $Parameters = @{ tenantFilter = $Tenant - displayName = $Settings.DisplayName + displayName = $DisplayName description = $Settings.Description userType = $userType DeploymentMode = $DeploymentMode @@ -95,9 +97,9 @@ function Invoke-CIPPStandardAutopilotProfile { Set-CIPPDefaultAPDeploymentProfile @Parameters if ($null -eq $CurrentConfig) { - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Created Autopilot profile '$($Settings.DisplayName)'" -sev Info + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Created Autopilot profile '$($DisplayName)'" -sev Info } else { - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Updated Autopilot profile '$($Settings.DisplayName)'" -sev Info + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Updated Autopilot profile '$($DisplayName)'" -sev Info } } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message @@ -108,19 +110,19 @@ function Invoke-CIPPStandardAutopilotProfile { } # Report - If ($Settings.report -eq $true) { + if ($Settings.report -eq $true) { $FieldValue = $StateIsCorrect -eq $true ? $true : $CurrentConfig Set-CIPPStandardsCompareField -FieldName 'standards.AutopilotProfile' -FieldValue $FieldValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'AutopilotProfile' -FieldValue [bool]$StateIsCorrect -StoreAs bool -Tenant $Tenant } # Alert - If ($Settings.alert -eq $true) { - If ($StateIsCorrect -eq $true) { - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Autopilot profile '$($Settings.DisplayName)' exists" -sev Info + if ($Settings.alert -eq $true) { + if ($StateIsCorrect -eq $true) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Autopilot profile '$($DisplayName)' exists" -sev Info } else { - Write-StandardsAlert -message "Autopilot profile '$($Settings.DisplayName)' do not match expected configuration" -object $CurrentConfig -tenant $Tenant -standardName 'AutopilotProfile' -standardId $Settings.standardId - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Autopilot profile '$($Settings.DisplayName)' do not match expected configuration" -sev Info + Write-StandardsAlert -message "Autopilot profile '$($DisplayName)' do not match expected configuration" -object $CurrentConfig -tenant $Tenant -standardName 'AutopilotProfile' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Autopilot profile '$($DisplayName)' do not match expected configuration" -sev Info } } } From 292d64f8f10435c0b705d7111c823e7436ff7d93 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 17 Jun 2025 13:13:40 -0400 Subject: [PATCH 118/160] update errors to reference Setup Wizard instead of SAM --- .../Public/GraphHelper/Get-NormalizedError.ps1 | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Modules/CIPPCore/Public/GraphHelper/Get-NormalizedError.ps1 b/Modules/CIPPCore/Public/GraphHelper/Get-NormalizedError.ps1 index 7833e7caa472..7c230e6e8d0b 100644 --- a/Modules/CIPPCore/Public/GraphHelper/Get-NormalizedError.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/Get-NormalizedError.ps1 @@ -3,6 +3,7 @@ function Get-NormalizedError { .FUNCTIONALITY Internal #> + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingEmptyCatchBlock', '', Justification = 'CIPP does not use this function to catch errors')] [CmdletBinding()] param ( [string]$message @@ -21,16 +22,16 @@ function Get-NormalizedError { #We need to check if the message is in one of these fields, and if so, return it. if ($JSONMsg.error.innererror.message) { - Write-Host "innererror.message found: $($JSONMsg.error.innererror.message)" + Write-Information "innererror.message found: $($JSONMsg.error.innererror.message)" $message = $JSONMsg.error.innererror.message } elseif ($JSONMsg.error.message) { - Write-Host "error.message found: $($JSONMsg.error.message)" + Write-Information "error.message found: $($JSONMsg.error.message)" $message = $JSONMsg.error.message } elseif ($JSONMsg.error.details.message) { - Write-Host "error.details.message found: $($JSONMsg.error.details.message)" + Write-Information "error.details.message found: $($JSONMsg.error.details.message)" $message = $JSONMsg.error.details.message } elseif ($JSONMsg.error.innererror.internalException.message) { - Write-Host "error.innererror.internalException.message found: $($JSONMsg.error.innererror.internalException.message)" + Write-Information "error.innererror.internalException.message found: $($JSONMsg.error.innererror.internalException.message)" $message = $JSONMsg.error.innererror.internalException.message } @@ -46,7 +47,7 @@ function Get-NormalizedError { '*User was not found.*' { 'The relationship between this tenant and the partner has been dissolved from the tenant side.' } '*AADSTS50020*' { 'AADSTS50020: The user you have used for your Secure Application Model is a guest in this tenant, or your are using GDAP and have not added the user to the correct group. Please delete the guest user to gain access to this tenant' } '*AADSTS50177' { 'AADSTS50177: The user you have used for your Secure Application Model is a guest in this tenant, or your are using GDAP and have not added the user to the correct group. Please delete the guest user to gain access to this tenant' } - '*invalid or malformed*' { 'The request is malformed. Have you finished the SAM Setup?' } + '*invalid or malformed*' { 'The request is malformed. Have you finished the Setup Wizard' } '*Windows Store repository apps feature is not supported for this tenant*' { 'This tenant does not have WinGet support available' } '*AADSTS650051*' { 'The application does not exist yet. Try again in 30 seconds.' } '*AppLifecycle_2210*' { 'Failed to call Intune APIs: Does the tenant have a license available?' } @@ -58,7 +59,7 @@ function Get-NormalizedError { '*Authentication failed. MFA required*' { 'Authentication failed. MFA required' } '*Your tenant is not licensed for this feature.*' { 'Required license not available for this tenant' } '*AADSTS65001*' { 'We cannot access this tenant as consent has not been given, please try refreshing the CPV permissions in the application settings menu.' } - '*AADSTS700082*' { 'The CIPP user access token has expired. Run the SAM Setup wizard to refresh your tokens.' } + '*AADSTS700082*' { 'The CIPP user access token has expired. Run the Setup Wizard to refresh your tokens.' } '*Account is not provisioned.' { 'The account is not provisioned. You do not the correct M365 license to access this information..' } '*AADSTS5000224*' { 'This resource is not available - Has this tenant been deleted?' } '*AADSTS53003*' { 'Access has been blocked by Conditional Access policies. Please check the Conditional Access configuration documentation' } @@ -66,7 +67,7 @@ function Get-NormalizedError { '*AADSTS9002313*' { 'The credentials used to connect to the Graph API are not available, please retry. If this issue persists you may need to execute the SAM wizard.' } '*One or more platform(s) is/are not configured for the customer. Please configure the platform before trying to purchase a SKU.*' { 'One or more platform(s) is/are not configured for the customer. Please configure the platform before trying to purchase a SKU.' } "One or more added object references already exist for the following modified properties: 'members'." { 'This user is already a member of the selected group.' } - Default { $message } + default { $message } } } From 9b6033dd32a58656da95628ca6ef9c3f429ca3ce Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 17 Jun 2025 14:56:15 -0400 Subject: [PATCH 119/160] improve reliability of editgroup --- .../Groups/Invoke-EditGroup.ps1 | 97 ++++++++++--------- 1 file changed, 50 insertions(+), 47 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-EditGroup.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-EditGroup.ps1 index 4657b6b265fe..86f1abdd4922 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-EditGroup.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-EditGroup.ps1 @@ -16,10 +16,11 @@ function Invoke-EditGroup { $UserObj = $Request.Body $GroupType = $UserObj.groupId.addedFields.groupType ? $UserObj.groupId.addedFields.groupType : $UserObj.groupType $GroupName = $UserObj.groupName ? $UserObj.groupName : $UserObj.groupId.addedFields.groupName - $OrgGroup = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/groups/$($UserObj.groupId)" -tenantid $UserObj.tenantFilter + $GroupId = $UserObj.groupId.value ?? $UserObj.groupId + $OrgGroup = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/groups/$($GroupId)" -tenantid $UserObj.tenantFilter $AddMembers = $UserObj.AddMember - $UserObj.groupId = $UserObj.groupId.value ?? $UserObj.groupId + $TenantId = $UserObj.tenantId ?? $UserObj.tenantFilter @@ -29,36 +30,38 @@ function Invoke-EditGroup { $ExoBulkRequests = [System.Collections.Generic.List[object]]::new() $ExoLogs = [System.Collections.Generic.List[object]]::new() - #Edit properties: - if ($GroupType -eq 'Distribution List' -or $GroupType -eq 'Mail-Enabled Security') { - $Params = @{ Identity = $UserObj.groupId; DisplayName = $UserObj.displayName; Description = $UserObj.description; name = $UserObj.mailNickname } - $ExoBulkRequests.Add(@{ - CmdletInput = @{ - CmdletName = 'Set-DistributionGroup' - Parameters = $Params - } - }) - $ExoLogs.Add(@{ - message = "Success - Edited group properties for $($GroupName) group. It might take some time to reflect the changes." - target = $UserObj.groupId - }) - } else { - $PatchObj = @{ - displayName = $UserObj.displayName - description = $UserObj.description - mailNickname = $UserObj.mailNickname - mailEnabled = $OrgGroup.mailEnabled - securityEnabled = $OrgGroup.securityEnabled - } - Write-Host "body: $($PatchObj | ConvertTo-Json -Depth 10 -Compress)" -ForegroundColor Yellow - if ($UserObj.membershipRules) { $PatchObj | Add-Member -MemberType NoteProperty -Name 'membershipRule' -Value $UserObj.membershipRules -Force } - try { - $patch = New-GraphPOSTRequest -type PATCH -uri "https://graph.microsoft.com/beta/groups/$($UserObj.groupId)" -tenantid $UserObj.tenantFilter -body ($PatchObj | ConvertTo-Json -Depth 10 -Compress) - $Results.Add("Success - Edited group properties for $($GroupName) group. It might take some time to reflect the changes.") - Write-LogMessage -headers $Headers -API $APIName -tenant $UserObj.tenantFilter -message "Edited group properties for $($GroupName) group" -Sev 'Info' - } catch { - $Results.Add("Error - Failed to edit group properties: $($_.Exception.Message)") - Write-LogMessage -headers $Headers -API $APIName -tenant $UserObj.tenantFilter -message "Failed to patch group: $($_.Exception.Message)" -Sev 'Error' + if ($UserObj.displayName -or $UserObj.description -or $UserObj.mailNickname -or $UserObj.membershipRules) { + #Edit properties: + if ($GroupType -eq 'Distribution List' -or $GroupType -eq 'Mail-Enabled Security') { + $Params = @{ Identity = $GroupId; DisplayName = $UserObj.displayName; Description = $UserObj.description; name = $UserObj.mailNickname } + $ExoBulkRequests.Add(@{ + CmdletInput = @{ + CmdletName = 'Set-DistributionGroup' + Parameters = $Params + } + }) + $ExoLogs.Add(@{ + message = "Success - Edited group properties for $($GroupName) group. It might take some time to reflect the changes." + target = $GroupId + }) + } else { + $PatchObj = @{ + displayName = $UserObj.displayName + description = $UserObj.description + mailNickname = $UserObj.mailNickname + mailEnabled = $OrgGroup.mailEnabled + securityEnabled = $OrgGroup.securityEnabled + } + Write-Host "body: $($PatchObj | ConvertTo-Json -Depth 10 -Compress)" -ForegroundColor Yellow + if ($UserObj.membershipRules) { $PatchObj | Add-Member -MemberType NoteProperty -Name 'membershipRule' -Value $UserObj.membershipRules -Force } + try { + $patch = New-GraphPOSTRequest -type PATCH -uri "https://graph.microsoft.com/beta/groups/$($GroupId)" -tenantid $UserObj.tenantFilter -body ($PatchObj | ConvertTo-Json -Depth 10 -Compress) + $Results.Add("Success - Edited group properties for $($GroupName) group. It might take some time to reflect the changes.") + Write-LogMessage -headers $Headers -API $APIName -tenant $UserObj.tenantFilter -message "Edited group properties for $($GroupName) group" -Sev 'Info' + } catch { + $Results.Add("Error - Failed to edit group properties: $($_.Exception.Message)") + Write-LogMessage -headers $Headers -API $APIName -tenant $UserObj.tenantFilter -message "Failed to patch group: $($_.Exception.Message)" -Sev 'Error' + } } } @@ -73,7 +76,7 @@ function Invoke-EditGroup { } if ($GroupType -eq 'Distribution List' -or $GroupType -eq 'Mail-Enabled Security') { - $Params = @{ Identity = $UserObj.groupId; Member = $Member; BypassSecurityGroupManagerCheck = $true } + $Params = @{ Identity = $GroupId; Member = $Member; BypassSecurityGroupManagerCheck = $true } # Write-Host ($UserObj | ConvertTo-Json -Depth 10) #Debugging line $ExoBulkRequests.Add(@{ CmdletInput = @{ @@ -94,7 +97,7 @@ function Invoke-EditGroup { $BulkRequests.Add(@{ id = "addMember-$Member" method = 'PATCH' - url = "groups/$($UserObj.groupId)" + url = "groups/$($GroupId)" body = $AddMemberBody headers = @{ 'Content-Type' = 'application/json' @@ -118,7 +121,7 @@ function Invoke-EditGroup { try { $Member = $_ if ($GroupType -eq 'Distribution list' -or $GroupType -eq 'Mail-Enabled Security') { - $Params = @{ Identity = $UserObj.groupId; Member = $Member.value; BypassSecurityGroupManagerCheck = $true } + $Params = @{ Identity = $GroupId; Member = $Member.value; BypassSecurityGroupManagerCheck = $true } $ExoBulkRequests.Add(@{ CmdletInput = @{ CmdletName = 'Add-DistributionGroupMember' @@ -146,7 +149,7 @@ function Invoke-EditGroup { $Member = $_.value $MemberID = $_.addedFields.id if ($GroupType -eq 'Distribution list' -or $GroupType -eq 'Mail-Enabled Security') { - $Params = @{ Identity = $UserObj.groupId; Member = $MemberID ; BypassSecurityGroupManagerCheck = $true } + $Params = @{ Identity = $GroupId; Member = $MemberID ; BypassSecurityGroupManagerCheck = $true } $ExoBulkRequests.Add(@{ CmdletInput = @{ CmdletName = 'Remove-DistributionGroupMember' @@ -174,7 +177,7 @@ function Invoke-EditGroup { $Member = $_.value $MemberID = $_.addedFields.id if ($GroupType -eq 'Distribution list' -or $GroupType -eq 'Mail-Enabled Security') { - $Params = @{ Identity = $UserObj.groupId; Member = $Member ; BypassSecurityGroupManagerCheck = $true } + $Params = @{ Identity = $GroupId; Member = $Member ; BypassSecurityGroupManagerCheck = $true } $ExoBulkRequests.Add(@{ CmdletInput = @{ CmdletName = 'Remove-DistributionGroupMember' @@ -189,7 +192,7 @@ function Invoke-EditGroup { $BulkRequests.Add(@{ id = "removeMember-$Member" method = 'DELETE' - url = "groups/$($UserObj.groupId)/members/$MemberID/`$ref" + url = "groups/$($GroupId)/members/$MemberID/`$ref" }) $GraphLogs.Add(@{ message = "Removed member $Member from $($GroupName) group" @@ -213,7 +216,7 @@ function Invoke-EditGroup { $BulkRequests.Add(@{ id = "addOwner-$Owner" method = 'POST' - url = "groups/$($UserObj.groupId)/owners/`$ref" + url = "groups/$($GroupId)/owners/`$ref" body = @{ '@odata.id' = $MemberODataBindString -f $ID } @@ -241,7 +244,7 @@ function Invoke-EditGroup { $BulkRequests.Add(@{ id = "removeOwner-$ID" method = 'DELETE' - url = "groups/$($UserObj.groupId)/owners/$ID/`$ref" + url = "groups/$($GroupId)/owners/$ID/`$ref" }) $GraphLogs.Add(@{ message = "Removed $($_.value) from $($GroupName) group" @@ -255,7 +258,7 @@ function Invoke-EditGroup { } if ($GroupType -in @( 'Distribution List', 'Mail-Enabled Security') -and ($AddOwners -or $RemoveOwners)) { - $CurrentOwners = New-ExoRequest -tenantid $TenantId -cmdlet 'Get-DistributionGroup' -cmdParams @{ Identity = $UserObj.groupId } -UseSystemMailbox $true | Select-Object -ExpandProperty ManagedBy + $CurrentOwners = New-ExoRequest -tenantid $TenantId -cmdlet 'Get-DistributionGroup' -cmdParams @{ Identity = $GroupId } -UseSystemMailbox $true | Select-Object -ExpandProperty ManagedBy $NewManagedBy = [system.collections.generic.list[string]]::new() foreach ($CurrentOwner in $CurrentOwners) { @@ -263,7 +266,7 @@ function Invoke-EditGroup { $OwnerToRemove = $RemoveOwners | Where-Object { $_.addedFields.id -eq $CurrentOwner } $ExoLogs.Add(@{ message = "Removed owner $($OwnerToRemove.label) from $($GroupName) group" - target = $UserObj.groupId + target = $GroupId }) continue } @@ -274,13 +277,13 @@ function Invoke-EditGroup { $NewManagedBy.Add($NewOwner.addedFields.id) $ExoLogs.Add(@{ message = "Added owner $($NewOwner.label) to $($GroupName) group" - target = $UserObj.groupId + target = $GroupId }) } } $NewManagedBy = $NewManagedBy | Sort-Object -Unique - $Params = @{ Identity = $UserObj.groupId; ManagedBy = $NewManagedBy } + $Params = @{ Identity = $GroupId; ManagedBy = $NewManagedBy } $ExoBulkRequests.Add(@{ CmdletInput = @{ CmdletName = 'Set-DistributionGroup' @@ -354,16 +357,16 @@ function Invoke-EditGroup { if ($UserObj.sendCopies -eq $true) { try { - $Params = @{ Identity = $UserObj.groupId; subscriptionEnabled = $true; AutoSubscribeNewMembers = $true } + $Params = @{ Identity = $GroupId; subscriptionEnabled = $true; AutoSubscribeNewMembers = $true } New-ExoRequest -tenantid $TenantId -cmdlet 'Set-UnifiedGroup' -cmdParams $Params -useSystemMailbox $true - $MemberParams = @{ Identity = $UserObj.groupId; LinkType = 'members' } + $MemberParams = @{ Identity = $GroupId; LinkType = 'members' } $Members = New-ExoRequest -tenantid $TenantId -cmdlet 'Get-UnifiedGroupLinks' -cmdParams $MemberParams $MemberSmtpAddresses = $Members | ForEach-Object { $_.PrimarySmtpAddress } if ($MemberSmtpAddresses) { - $subscriberParams = @{ Identity = $UserObj.groupId; LinkType = 'subscribers'; Links = @($MemberSmtpAddresses | Where-Object { $_ }) } + $subscriberParams = @{ Identity = $GroupId; LinkType = 'subscribers'; Links = @($MemberSmtpAddresses | Where-Object { $_ }) } New-ExoRequest -tenantid $TenantId -cmdlet 'Add-UnifiedGroupLinks' -cmdParams $subscriberParams -Anchor $UserObj.mail } From 5c483a2c06ddcf31fbfd78ebe124f6e06b6f616f Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 17 Jun 2025 19:20:24 -0400 Subject: [PATCH 120/160] Fix AssignedTo property --- .../HTTP Functions/Teams-Sharepoint/Invoke-ListTeamsVoice.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListTeamsVoice.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListTeamsVoice.ps1 index 2bfd473f902c..a35801474473 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListTeamsVoice.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListTeamsVoice.ps1 @@ -1,6 +1,6 @@ using namespace System.Net -Function Invoke-ListTeamsVoice { +function Invoke-ListTeamsVoice { <# .FUNCTIONALITY Entrypoint @@ -24,7 +24,7 @@ Function Invoke-ListTeamsVoice { Write-Host "Getting page $Skip" $data = (New-TeamsAPIGetRequest -uri "https://api.interfaces.records.teams.microsoft.com/Skype.TelephoneNumberMgmt/Tenants/$($TenantId)/telephone-numbers?skip=$($Skip)&locale=en-US&top=999" -tenantid $TenantFilter).TelephoneNumbers | ForEach-Object { Write-Host 'Reached the loop' - $CompleteRequest = $_ | Select-Object *, @{Name = 'AssignedTo'; Expression = { $users | Where-Object -Property id -EQ $_.AssignedTo.id } } + $CompleteRequest = $_ | Select-Object *, @{Name = 'AssignedTo'; Expression = { $users | Where-Object -Property id -EQ $_.TargetId } } if ($CompleteRequest.AcquisitionDate) { $CompleteRequest.AcquisitionDate = $_.AcquisitionDate -split 'T' | Select-Object -First 1 } else { From 8a8a06d41d93d576072e23c7b0cfe22366ec3a07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Wed, 18 Jun 2025 19:06:31 +0200 Subject: [PATCH 121/160] Enhance New-CIPPTAP function to support additional parameters for TAP creation, including LifetimeInMinutes, IsUsableOnce, and StartDateTime. Update logging to reflect new parameters and improve error handling in Invoke-ExecCreateTAP function. Refactor logging in New-CIPPTAP function to use List for better performance and clarity. Update log messages to include UTC for start times and improve handling of empty request bodies. Refactor New-CIPPTAP function to enhance logging by capturing response values for TAP creation. Simplify log message construction and ensure accurate representation of start times. Remove redundant code for improved clarity. Ternary operators to make code cleaner --- .../Users/Invoke-ExecCreateTAP.ps1 | 18 +++++- Modules/CIPPCore/Public/New-CIPPTAP.ps1 | 61 ++++++++++++++++--- 2 files changed, 70 insertions(+), 9 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecCreateTAP.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecCreateTAP.ps1 index d8b6022c735d..ef3282aa4243 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecCreateTAP.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecCreateTAP.ps1 @@ -17,9 +17,23 @@ Function Invoke-ExecCreateTAP { # Interact with query parameters or the body of the request. $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter $UserID = $Request.Query.ID ?? $Request.Body.ID + $LifetimeInMinutes = $Request.Query.lifetimeInMinutes ?? $Request.Body.lifetimeInMinutes + $IsUsableOnce = $Request.Query.isUsableOnce ?? $Request.Body.isUsableOnce + $StartDateTime = $Request.Query.startDateTime ?? $Request.Body.startDateTime try { - $TAPResult = New-CIPPTAP -userid $UserID -TenantFilter $TenantFilter -APIName $APIName -Headers $Headers + # Create parameter hashtable for splatting + $TAPParams = @{ + UserID = $UserID + TenantFilter = $TenantFilter + APIName = $APIName + Headers = $Headers + LifetimeInMinutes = $LifetimeInMinutes + IsUsableOnce = $IsUsableOnce + StartDateTime = $StartDateTime + } + + $TAPResult = New-CIPPTAP @TAPParams # Create results array with both TAP and UserID as separate items $Results = @( @@ -33,7 +47,7 @@ Function Invoke-ExecCreateTAP { $StatusCode = [HttpStatusCode]::OK } catch { - $Results = Get-NormalizedError -message $_.Exception.Message + $Results = $_.Exception.Message $StatusCode = [HttpStatusCode]::InternalServerError } diff --git a/Modules/CIPPCore/Public/New-CIPPTAP.ps1 b/Modules/CIPPCore/Public/New-CIPPTAP.ps1 index 1d934411dff1..120a81ff1264 100644 --- a/Modules/CIPPCore/Public/New-CIPPTAP.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPTAP.ps1 @@ -1,28 +1,75 @@ function New-CIPPTAP { [CmdletBinding()] param ( - $userid, + $UserID, $TenantFilter, $APIName = 'Create TAP', - $Headers + $Headers, + $LifetimeInMinutes, + [bool]$IsUsableOnce, + $StartDateTime ) try { - $GraphRequest = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/users/$($userid)/authentication/temporaryAccessPassMethods" -tenantid $TenantFilter -type POST -body '{}' -verbose - Write-LogMessage -headers $Headers -API $APIName -message "Created Temporary Access Password (TAP) for $userid" -Sev 'Info' -tenant $TenantFilter + # Build the request body based on provided parameters + $RequestBody = @{} + + if ($LifetimeInMinutes) { + $RequestBody.lifetimeInMinutes = [int]$LifetimeInMinutes + } + + if ($null -ne $IsUsableOnce) { + $RequestBody.isUsableOnce = $IsUsableOnce + } + + if ($StartDateTime) { + # Convert Unix timestamp to DateTime if it's a number + if ($StartDateTime -match '^\d+$') { + $dateTime = [DateTimeOffset]::FromUnixTimeSeconds([int]$StartDateTime).DateTime + $RequestBody.startDateTime = Get-Date $dateTime -Format 'o' + } else { + # If it's already a date string, format it properly + $dateTime = Get-Date $StartDateTime + $RequestBody.startDateTime = Get-Date $dateTime -Format 'o' + } + } + + # Convert request body to JSON + $BodyJson = if ($RequestBody) { $RequestBody | ConvertTo-Json } else { '{}' } + $GraphRequest = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/users/$($UserID)/authentication/temporaryAccessPassMethods" -tenantid $TenantFilter -type POST -body $BodyJson -verbose + + # Build log message parts based on actual response values + $logParts = [System.Collections.Generic.List[string]]::new() + $logParts.Add("Lifetime: $($GraphRequest.lifetimeInMinutes) minutes") + + $logParts.Add($GraphRequest.isUsableOnce ? 'one-time use' : 'multi-use') + + $logParts.Add($StartDateTime ? "starts at $(Get-Date $GraphRequest.startDateTime -Format 'yyyy-MM-dd HH:mm:ss') UTC" : 'starts immediately') + + # Create parameter string for logging + $paramString = ' with ' + ($logParts -join ', ') + + Write-LogMessage -headers $Headers -API $APIName -message "Created Temporary Access Password (TAP) for $UserID$paramString" -Sev 'Info' -tenant $TenantFilter + + # Build result text with parameters + $resultText = "The TAP for $UserID is $($GraphRequest.temporaryAccessPass) - This TAP is usable for the next $($GraphRequest.LifetimeInMinutes) minutes" + $resultText += $GraphRequest.isUsableOnce ? ' (one-time use only)' : '' + $resultText += $StartDateTime ? " starting at $(Get-Date $GraphRequest.startDateTime -Format 'yyyy-MM-dd HH:mm:ss') UTC" : '' + return @{ - resultText = "The TAP for $userid is $($GraphRequest.temporaryAccessPass) - This TAP is usable for the next $($GraphRequest.LifetimeInMinutes) minutes" - userid = $userid + resultText = $resultText + userId = $UserID copyField = $GraphRequest.temporaryAccessPass temporaryAccessPass = $GraphRequest.temporaryAccessPass lifetimeInMinutes = $GraphRequest.LifetimeInMinutes startDateTime = $GraphRequest.startDateTime + isUsableOnce = $GraphRequest.isUsableOnce state = 'success' } } catch { $ErrorMessage = Get-CippException -Exception $_ - $Result = "Failed to create Temporary Access Password (TAP) for $($userid): $($ErrorMessage.NormalizedError)" + $Result = "Failed to create Temporary Access Password (TAP) for $($UserID): $($ErrorMessage.NormalizedError)" Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Error' -tenant $TenantFilter -LogData $ErrorMessage throw $Result } From 4d562fab9f19cef76a001654018c1d3ca9d1de84 Mon Sep 17 00:00:00 2001 From: ngms-psh Date: Wed, 18 Jun 2025 21:48:18 +0200 Subject: [PATCH 122/160] Fix report for QuarantineTemplate --- .../Tenant/Standards/Invoke-ListStandardsCompare.ps1 | 12 ++++++++++++ .../Invoke-CIPPStandardQuarantineTemplate.ps1 | 5 +++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListStandardsCompare.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListStandardsCompare.ps1 index 72455cfce276..21b8b79272c7 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListStandardsCompare.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListStandardsCompare.ps1 @@ -50,6 +50,18 @@ function Invoke-ListStandardsCompare { $FieldName = $Standard.RowKey $FieldValue = $Standard.Value $Tenant = $Standard.PartitionKey + + # decode field names that are hex encoded (e.g. QuarantineTemplates) + if ($FieldName -match '^(standards\.QuarantineTemplate\.)(.+)$') { + $Prefix = $Matches[1] + $HexEncodedName = $Matches[2] + $Chars = [System.Collections.Generic.List[char]]::new() + for ($i = 0; $i -lt $HexEncodedName.Length; $i += 2) { + $Chars.Add([char][Convert]::ToInt32($HexEncodedName.Substring($i,2),16)) + } + $FieldName = "$Prefix$(-join $Chars)" + } + if ($FieldValue -is [System.Boolean]) { $FieldValue = [bool]$FieldValue } elseif ($FieldValue -like '*{*') { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineTemplate.ps1 index 83410760f70f..4f76c8f4378c 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineTemplate.ps1 @@ -183,9 +183,10 @@ function Invoke-CIPPStandardQuarantineTemplate { } if ($true -in $Settings.report) { - # This could do with an improvement. But will work for now or else reporting could be disabled for now foreach ($Policy in $CompareList | Where-Object -Property report -EQ $true) { - Set-CIPPStandardsCompareField -FieldName "standards.QuarantineTemplate" -FieldValue $Policy.StateIsCorrect -TenantFilter $Tenant + # Convert displayName to hex to avoid invalid characters "/, \, #, ?" which are not allowed in RowKey, but "\, #, ?" can be used in quarantine displayName + $HexName = -join ($Policy.displayName.ToCharArray() | ForEach-Object { '{0:X2}' -f [int][char]$_ }) + Set-CIPPStandardsCompareField -FieldName "standards.QuarantineTemplate.$HexName" -FieldValue $Policy.StateIsCorrect -TenantFilter $Tenant } } } From 5d35bf94a8a3443257a4ec8503c00065c787853d Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 18 Jun 2025 17:36:06 -0400 Subject: [PATCH 123/160] improve tenant access check --- .../CIPP/Settings/Invoke-ExecAccessChecks.ps1 | 24 +++++----- .../CIPPCore/Public/Test-CIPPAccessTenant.ps1 | 48 +++++++++++++++---- 2 files changed, 53 insertions(+), 19 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecAccessChecks.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecAccessChecks.ps1 index 612918dfe20b..ce2d86ce2dec 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecAccessChecks.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecAccessChecks.ps1 @@ -1,6 +1,6 @@ using namespace System.Net -Function Invoke-ExecAccessChecks { +function Invoke-ExecAccessChecks { <# .FUNCTIONALITY Entrypoint @@ -50,16 +50,17 @@ Function Invoke-ExecAccessChecks { $Results = foreach ($Tenant in $Tenants) { $TenantCheck = $AccessChecks | Where-Object -Property RowKey -EQ $Tenant.customerId | Select-Object -Property Data $TenantResult = [PSCustomObject]@{ - TenantId = $Tenant.customerId - TenantName = $Tenant.displayName - DefaultDomainName = $Tenant.defaultDomainName - GraphStatus = 'Not run yet' - ExchangeStatus = 'Not run yet' - GDAPRoles = '' - MissingRoles = '' - LastRun = '' - GraphTest = '' - ExchangeTest = '' + TenantId = $Tenant.customerId + TenantName = $Tenant.displayName + DefaultDomainName = $Tenant.defaultDomainName + GraphStatus = 'Not run yet' + ExchangeStatus = 'Not run yet' + GDAPRoles = '' + MissingRoles = '' + LastRun = '' + GraphTest = '' + ExchangeTest = '' + OrgManagementRoles = @() } if ($TenantCheck) { $Data = @($TenantCheck.Data | ConvertFrom-Json -ErrorAction Stop) @@ -70,6 +71,7 @@ Function Invoke-ExecAccessChecks { $TenantResult.LastRun = $Data.LastRun $TenantResult.GraphTest = $Data.GraphTest $TenantResult.ExchangeTest = $Data.ExchangeTest + $TenantResult.OrgManagementRoles = $Data.OrgManagementRoles } $TenantResult } diff --git a/Modules/CIPPCore/Public/Test-CIPPAccessTenant.ps1 b/Modules/CIPPCore/Public/Test-CIPPAccessTenant.ps1 index a54a22d5cc2e..d23f73a5bd2e 100644 --- a/Modules/CIPPCore/Public/Test-CIPPAccessTenant.ps1 +++ b/Modules/CIPPCore/Public/Test-CIPPAccessTenant.ps1 @@ -48,14 +48,15 @@ function Test-CIPPAccessTenant { $ExchangeStatus = $false $Results = [PSCustomObject]@{ - TenantName = $Tenant.defaultDomainName - GraphStatus = $false - GraphTest = '' - ExchangeStatus = $false - ExchangeTest = '' - GDAPRoles = '' - MissingRoles = '' - LastRun = (Get-Date).ToUniversalTime() + TenantName = $Tenant.defaultDomainName + GraphStatus = $false + GraphTest = '' + ExchangeStatus = $false + ExchangeTest = '' + GDAPRoles = '' + MissingRoles = '' + OrgManagementRoles = @() + LastRun = (Get-Date).ToUniversalTime() } $AddedText = '' @@ -105,6 +106,37 @@ function Test-CIPPAccessTenant { $null = New-ExoRequest -tenantid $Tenant.customerId -cmdlet 'Get-OrganizationConfig' -ErrorAction Stop $ExchangeStatus = $true $ExchangeTest = 'Successfully connected to Exchange' + + # Get the Exchange role definitions and assignments for the Organization Management role group + $Requests = @( + @{ + id = 'roleDefinitions' + method = 'GET' + url = 'roleManagement/exchange/roleDefinitions?$top=999' + } + @{ + id = 'roleAssignments' + method = 'GET' + url = "roleManagement/exchange/roleAssignments?`$filter=principalId eq '/RoleGroups/Organization Management'&`$top=999" + } + ) + + $ExchangeRoles = New-GraphBulkRequest -tenantid $Tenant.customerId -Requests $Requests + + # Get results and expand assigments with role definitions + $RoleDefinitions = ($ExchangeRoles | Where-Object -Property id -EQ 'roleDefinitions').body.value | Select-Object -Property id, displayName, description, isBuiltIn, isEnabled + $RoleAssignments = ($ExchangeRoles | Where-Object -Property id -EQ 'roleAssignments').body.value + $OrgManagementAssignments = $RoleAssignments | Where-Object -Property principalId -EQ '/RoleGroups/Organization Management' | Sort-Object -Property roleDefinitionId -Unique + $OrgManagementRoles = $OrgManagementAssignments | ForEach-Object { + $RoleDefinitions | Where-Object -Property id -EQ $_.roleDefinitionId + } | Sort-Object -Property displayName + + Write-Warning "Found $($OrgManagementRoles.Count) Organization Management role assignments in Exchange" + $Results.OrgManagementRoles = $OrgManagementRoles + + # TODO: Get list of known good roles and compare against the found roles + + } catch { $ErrorMessage = Get-CippException -Exception $_ $ReportedError = ($_.ErrorDetails | ConvertFrom-Json -ErrorAction SilentlyContinue) From 040aa698ae90137f45320a29078c2a44f6e203e0 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 18 Jun 2025 20:43:32 -0400 Subject: [PATCH 124/160] exchange permission repair --- .../CIPP/Settings/Invoke-ExecAccessChecks.ps1 | 28 +++--- .../Invoke-ExecExchangeRoleRepair.ps1 | 94 +++++++++++++++++++ .../Public/OrganizationManagementRoles.json | 51 ++++++++++ .../CIPPCore/Public/Test-CIPPAccessTenant.ps1 | 73 +++++++------- 4 files changed, 195 insertions(+), 51 deletions(-) create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExchangeRoleRepair.ps1 create mode 100644 Modules/CIPPCore/Public/OrganizationManagementRoles.json diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecAccessChecks.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecAccessChecks.ps1 index ce2d86ce2dec..e584f92092a9 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecAccessChecks.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecAccessChecks.ps1 @@ -50,17 +50,19 @@ function Invoke-ExecAccessChecks { $Results = foreach ($Tenant in $Tenants) { $TenantCheck = $AccessChecks | Where-Object -Property RowKey -EQ $Tenant.customerId | Select-Object -Property Data $TenantResult = [PSCustomObject]@{ - TenantId = $Tenant.customerId - TenantName = $Tenant.displayName - DefaultDomainName = $Tenant.defaultDomainName - GraphStatus = 'Not run yet' - ExchangeStatus = 'Not run yet' - GDAPRoles = '' - MissingRoles = '' - LastRun = '' - GraphTest = '' - ExchangeTest = '' - OrgManagementRoles = @() + TenantId = $Tenant.customerId + TenantName = $Tenant.displayName + DefaultDomainName = $Tenant.defaultDomainName + GraphStatus = 'Not run yet' + ExchangeStatus = 'Not run yet' + GDAPRoles = '' + MissingRoles = '' + LastRun = '' + GraphTest = '' + ExchangeTest = '' + OrgManagementRoles = @() + OrgManagementRolesMissing = @() + OrgManagementRepairNeeded = $false } if ($TenantCheck) { $Data = @($TenantCheck.Data | ConvertFrom-Json -ErrorAction Stop) @@ -71,7 +73,9 @@ function Invoke-ExecAccessChecks { $TenantResult.LastRun = $Data.LastRun $TenantResult.GraphTest = $Data.GraphTest $TenantResult.ExchangeTest = $Data.ExchangeTest - $TenantResult.OrgManagementRoles = $Data.OrgManagementRoles + $TenantResult.OrgManagementRoles = $Data.OrgManagementRoles ? @($Data.OrgManagementRoles) : @() + $TenantResult.OrgManagementRolesMissing = $Data.OrgManagementRolesMissing ? @($Data.OrgManagementRolesMissing) : @() + $TenantResult.OrgManagementRepairNeeded = $Data.OrgManagementRolesMissing.Count -gt 0 } $TenantResult } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExchangeRoleRepair.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExchangeRoleRepair.ps1 new file mode 100644 index 000000000000..9a1433923576 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExchangeRoleRepair.ps1 @@ -0,0 +1,94 @@ +function Invoke-ExecExchangeRoleRepair { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + CIPP.AppSettings.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $Headers = $Request.Headers + + $TenantId = $Request.Query.tenantId ?? $Request.Body.tenantId + $Tenant = Get-Tenants -TenantFilter $TenantId + + try { + Write-Information "Starting Exchange Organization Management role repair for tenant: $($Tenant.defaultDomainName)" + $OrgManagementRoles = New-ExoRequest -tenantid $Tenant.customerId -cmdlet 'Get-ManagementRoleAssignment' -cmdParams @{ RoleAssignee = 'Organization Management'; Delegating = $false } | Select-Object -Property Role, Guid + Write-Information "Found $($OrgManagementRoles.Count) Organization Management roles in Exchange" + + $RoleDefinitions = New-GraphGetRequest -tenantid $Tenant.customerId -uri 'https://graph.microsoft.com/beta/roleManagement/exchange/roleDefinitions' + Write-Information "Found $($RoleDefinitions.Count) Exchange role definitions" + + $BasePath = Get-Module -Name 'CIPPCore' | Select-Object -ExpandProperty ModuleBase + $AllOrgManagementRoles = Get-Content -Path "$BasePath\Public\OrganizationManagementRoles.json" -ErrorAction Stop | ConvertFrom-Json + + $AvailableRoles = $RoleDefinitions | Where-Object -Property displayName -In $AllOrgManagementRoles | Select-Object -Property displayName, id, description + Write-Information "Found $($AvailableRoles.Count) available Organization Management roles in Exchange" + $MissingOrgMgmtRoles = $AvailableRoles | Where-Object { $OrgManagementRoles.Role -notcontains $_.displayName } + + if ($MissingOrgMgmtRoles.Count -gt 0) { + $Requests = foreach ($Role in $MissingOrgMgmtRoles) { + [PSCustomObject]@{ + id = $Role.id + method = 'POST' + url = 'roleManagement/exchange/roleAssignments' + body = @{ + principalId = '/RoleGroups/Organization Management' + roleDefinitionId = $Role.id + directoryScopeId = '/' + appScopeId = $null + } + headers = @{ + 'Content-Type' = 'application/json' + } + } + } + + $RepairResults = New-GraphBulkRequest -tenantid $Tenant.customerId -Requests @($Requests) -asapp $true + $RepairSuccess = $RepairResults.status -eq 201 + if ($RepairSuccess) { + $Results = @{ + state = 'success' + resultText = "Successfully repaired the missing Organization Management roles: $($MissingOrgMgmtRoles.displayName -join ', ')" + } + Write-LogMessage -headers $Headers -tenant $Tenant.defaultDomainName -tenantid $Tenant.customerId -Message "Successfully repaired the missing Organization Management roles: $($MissingOrgMgmtRoles.displayName -join ', '). Run another Tenant Access check after waiting a bit for replication." -sev 'Info' + } else { + # Get roles that failed to repair + $FailedRoles = $RepairResults | Where-Object { $_.status -ne 201 } | ForEach-Object { + $RoleId = $_.id + $Role = $MissingOrgMgmtRoles | Where-Object { $_.id -eq $RoleId } + $Role.displayName + } + $PermissionError = $false + if ($RepairResults.status -in (401, 403, 500)) { + $PermissionError = $true + } + $Results = @{ + state = 'error' + resultText = "Failed to repair the missing Organization Management roles: $($FailedRoles -join ', ').$(if ($PermissionError) { " This may be due to insufficient permissions. The required Graph Permission is 'RoleManagement.ReadWrite.Exchange'" })" + } + Write-LogMessage -headers $Headers -tenant $Tenant.defaultDomainName -tenantid $Tenant.customerId -Message "Failed to repair the missing Organization Management roles: $($FailedRoles -join ', ')" -sev 'Error' + } + } else { + $Results = @{ + state = 'success' + resultText = 'No missing Organization Management roles found.' + } + } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-Warning "Exception during Exchange Organization Management role repair: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -tenant $Tenant.defaultDomainName -tenantid $Tenant.customerId -Message "Exchange Organization Management role repair failed: $($ErrorMessage.NormalizedError)" -sev 'Error' -LogData $ErrorMessage + $Results = @{ + state = 'error' + resultText = "Exchange Organization Management role repair failed: $($ErrorMessage.NormalizedError)" + } + } + + Push-OutputBinding -Name 'Response' -Value ([HttpResponseContext]@{ + StatusCode = [System.Net.HttpStatusCode]::OK + Body = $Results + }) +} diff --git a/Modules/CIPPCore/Public/OrganizationManagementRoles.json b/Modules/CIPPCore/Public/OrganizationManagementRoles.json new file mode 100644 index 000000000000..86b72bf503e3 --- /dev/null +++ b/Modules/CIPPCore/Public/OrganizationManagementRoles.json @@ -0,0 +1,51 @@ +[ + "Audit Logs", + "Communication Compliance Admin", + "Communication Compliance Investigation", + "Compliance Admin", + "Data Loss Prevention", + "Distribution Groups", + "E-Mail Address Policies", + "Federated Sharing", + "Information Protection Admin", + "Information Protection Analyst", + "Information Protection Investigator", + "Information Protection Reader", + "Information Rights Management", + "Insider Risk Management Admin", + "Insider Risk Management Investigation", + "Journaling", + "Legal Hold", + "Mail Enabled Public Folders", + "Mail Recipient Creation", + "Mail Recipients", + "Mail Tips", + "Message Tracking", + "Migration", + "Move Mailboxes", + "Org Custom Apps", + "Org Marketplace Apps", + "Organization Client Access", + "Organization Configuration", + "Organization Transport Settings", + "PlacesBuildingManagement", + "PlacesDeskManagement", + "Privacy Management Admin", + "Privacy Management Investigation", + "Public Folders", + "Recipient Policies", + "Remote and Accepted Domains", + "Reset Password", + "Retention Management", + "Role Management", + "Security Admin", + "Security Group Creation and Membership", + "Security Reader", + "TenantPlacesManagement", + "Transport Hygiene", + "Transport Rules", + "User Options", + "View-Only Audit Logs", + "View-Only Configuration", + "View-Only Recipients" +] \ No newline at end of file diff --git a/Modules/CIPPCore/Public/Test-CIPPAccessTenant.ps1 b/Modules/CIPPCore/Public/Test-CIPPAccessTenant.ps1 index d23f73a5bd2e..bae5f70793b0 100644 --- a/Modules/CIPPCore/Public/Test-CIPPAccessTenant.ps1 +++ b/Modules/CIPPCore/Public/Test-CIPPAccessTenant.ps1 @@ -48,15 +48,16 @@ function Test-CIPPAccessTenant { $ExchangeStatus = $false $Results = [PSCustomObject]@{ - TenantName = $Tenant.defaultDomainName - GraphStatus = $false - GraphTest = '' - ExchangeStatus = $false - ExchangeTest = '' - GDAPRoles = '' - MissingRoles = '' - OrgManagementRoles = @() - LastRun = (Get-Date).ToUniversalTime() + TenantName = $Tenant.defaultDomainName + GraphStatus = $false + GraphTest = '' + ExchangeStatus = $false + ExchangeTest = '' + GDAPRoles = '' + MissingRoles = '' + OrgManagementRoles = @() + OrgManagementRolesMissing = @() + LastRun = (Get-Date).ToUniversalTime() } $AddedText = '' @@ -104,39 +105,32 @@ function Test-CIPPAccessTenant { try { $null = New-ExoRequest -tenantid $Tenant.customerId -cmdlet 'Get-OrganizationConfig' -ErrorAction Stop - $ExchangeStatus = $true - $ExchangeTest = 'Successfully connected to Exchange' - - # Get the Exchange role definitions and assignments for the Organization Management role group - $Requests = @( - @{ - id = 'roleDefinitions' - method = 'GET' - url = 'roleManagement/exchange/roleDefinitions?$top=999' - } - @{ - id = 'roleAssignments' - method = 'GET' - url = "roleManagement/exchange/roleAssignments?`$filter=principalId eq '/RoleGroups/Organization Management'&`$top=999" - } - ) - - $ExchangeRoles = New-GraphBulkRequest -tenantid $Tenant.customerId -Requests $Requests - - # Get results and expand assigments with role definitions - $RoleDefinitions = ($ExchangeRoles | Where-Object -Property id -EQ 'roleDefinitions').body.value | Select-Object -Property id, displayName, description, isBuiltIn, isEnabled - $RoleAssignments = ($ExchangeRoles | Where-Object -Property id -EQ 'roleAssignments').body.value - $OrgManagementAssignments = $RoleAssignments | Where-Object -Property principalId -EQ '/RoleGroups/Organization Management' | Sort-Object -Property roleDefinitionId -Unique - $OrgManagementRoles = $OrgManagementAssignments | ForEach-Object { - $RoleDefinitions | Where-Object -Property id -EQ $_.roleDefinitionId - } | Sort-Object -Property displayName - Write-Warning "Found $($OrgManagementRoles.Count) Organization Management role assignments in Exchange" + $OrgManagementRoles = New-ExoRequest -tenantid $Tenant.customerId -cmdlet 'Get-ManagementRoleAssignment' -cmdParams @{ RoleAssignee = 'Organization Management'; Delegating = $false } | Select-Object -Property Role, Guid + Write-Information "Found $($OrgManagementRoles.Count) Organization Management roles in Exchange" $Results.OrgManagementRoles = $OrgManagementRoles - # TODO: Get list of known good roles and compare against the found roles - - + $RoleDefinitions = New-GraphGetRequest -tenantid $Tenant.customerId -uri 'https://graph.microsoft.com/beta/roleManagement/exchange/roleDefinitions' + Write-Information "Found $($RoleDefinitions.Count) Exchange role definitions" + + $BasePath = Get-Module -Name 'CIPPCore' | Select-Object -ExpandProperty ModuleBase + $AllOrgManagementRoles = Get-Content -Path "$BasePath\Public\OrganizationManagementRoles.json" -ErrorAction Stop | ConvertFrom-Json + Write-Information "Loaded all Organization Management roles from $BasePath\Public\OrganizationManagementRoles.json" + + $AvailableRoles = $RoleDefinitions | Where-Object -Property displayName -In $AllOrgManagementRoles | Select-Object -Property displayName, id, description + Write-Information "Found $($AvailableRoles.Count) available Organization Management roles in Exchange" + $MissingOrgMgmtRoles = $AvailableRoles | Where-Object { $OrgManagementRoles.Role -notcontains $_.displayName } + if (($MissingOrgMgmtRoles | Measure-Object).Count -gt 0) { + $Results.OrgManagementRolesMissing = $MissingOrgMgmtRoles + Write-Warning "Found $($MissingRoles.Count) missing Organization Management roles in Exchange" + $ExchangeStatus = $false + $ExchangeTest = 'Connected to Exchange but missing permissions in Organization Management. This may impact the ability to manage Exchange features' + Write-LogMessage -headers $Headers -API $APINAME -tenant $tenant.defaultDomainName -message 'Tenant access check for Exchange failed: Missing Organization Management roles' -Sev 'Warning' -LogData $MissingOrgMgmtRoles + } else { + Write-Warning 'All available Organization Management roles are present in Exchange' + $ExchangeStatus = $true + $ExchangeTest = 'Successfully connected to Exchange' + } } catch { $ErrorMessage = Get-CippException -Exception $_ $ReportedError = ($_.ErrorDetails | ConvertFrom-Json -ErrorAction SilentlyContinue) @@ -145,6 +139,7 @@ function Test-CIPPAccessTenant { $ExchangeTest = "Failed to connect to Exchange: $($ErrorMessage.NormalizedError)" Write-LogMessage -headers $Headers -API $APINAME -tenant $tenant.defaultDomainName -message "Tenant access check for Exchange failed: $($ErrorMessage.NormalizedError) " -Sev 'Error' -LogData $ErrorMessage + Write-Warning "Failed to connect to Exchange: $($_.Exception.Message)" } if ($GraphStatus -and $ExchangeStatus) { From 270897ef39ca0c2d232b3f4ab7a40b99929168c5 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 18 Jun 2025 21:10:36 -0400 Subject: [PATCH 125/160] Update Invoke-ExecExchangeRoleRepair.ps1 --- .../CIPP/Settings/Invoke-ExecExchangeRoleRepair.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExchangeRoleRepair.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExchangeRoleRepair.ps1 index 9a1433923576..76cea5d8a031 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExchangeRoleRepair.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExchangeRoleRepair.ps1 @@ -67,7 +67,7 @@ function Invoke-ExecExchangeRoleRepair { } $Results = @{ state = 'error' - resultText = "Failed to repair the missing Organization Management roles: $($FailedRoles -join ', ').$(if ($PermissionError) { " This may be due to insufficient permissions. The required Graph Permission is 'RoleManagement.ReadWrite.Exchange'" })" + resultText = "Failed to repair the missing Organization Management roles: $($FailedRoles -join ', ').$(if ($PermissionError) { " This may be due to insufficient permissions. The required Graph Permission is 'Application - RoleManagement.ReadWrite.Exchange'" })" } Write-LogMessage -headers $Headers -tenant $Tenant.defaultDomainName -tenantid $Tenant.customerId -Message "Failed to repair the missing Organization Management roles: $($FailedRoles -join ', ')" -sev 'Error' } From 6b842b59471aeacd7fb61d667333d0fc392d8ec6 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 18 Jun 2025 21:18:35 -0400 Subject: [PATCH 126/160] add RoleManagement.ReadWrite.Exchange --- Modules/CIPPCore/Public/SAMManifest.json | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/Modules/CIPPCore/Public/SAMManifest.json b/Modules/CIPPCore/Public/SAMManifest.json index 510350f1d35b..12702b4c6beb 100644 --- a/Modules/CIPPCore/Public/SAMManifest.json +++ b/Modules/CIPPCore/Public/SAMManifest.json @@ -143,6 +143,10 @@ "id": "292d869f-3427-49a8-9dab-8c70152b74e9", "type": "Role" }, + { + "id": "2cb92fee-97a3-4034-8702-24a6f5d0d1e9", + "type": "Role" + }, { "id": "b6890674-9dd5-4e42-bb15-5af07f541ae1", "type": "Role" @@ -191,6 +195,10 @@ "id": "2a60023f-3219-47ad-baa4-40e17cd02a1d", "type": "Role" }, + { + "id": "025d3225-3f02-4882-b4c0-cd5b541a4e80", + "type": "Role" + }, { "id": "04c55753-2244-4c25-87fc-704ab82a4f69", "type": "Role" @@ -399,6 +407,10 @@ "id": "46ca0847-7e6b-426e-9775-ea810a948356", "type": "Scope" }, + { + "id": "346c19ff-3fb2-4e81-87a0-bac9e33990c1", + "type": "Scope" + }, { "id": "e67e6727-c080-415e-b521-e3f35d5248e9", "type": "Scope" @@ -558,14 +570,6 @@ { "id": "b7887744-6746-4312-813d-72daeaee7e2d", "type": "Scope" - }, - { - "id": "346c19ff-3fb2-4e81-87a0-bac9e33990c1", - "type": "Scope" - }, - { - "id": "2cb92fee-97a3-4034-8702-24a6f5d0d1e9", - "type": "Role" } ] }, @@ -639,4 +643,4 @@ ] } ] -} +} \ No newline at end of file From 5435419b23b96a4df47e205df264b5cc2d1d210b Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 19 Jun 2025 00:22:46 -0400 Subject: [PATCH 127/160] Create CIPP-Permissions.json --- CIPP-Permissions.json | 789 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 789 insertions(+) create mode 100644 CIPP-Permissions.json diff --git a/CIPP-Permissions.json b/CIPP-Permissions.json new file mode 100644 index 000000000000..96368090acd7 --- /dev/null +++ b/CIPP-Permissions.json @@ -0,0 +1,789 @@ +[ + { + "AppId": "aeb86249-8ea3-49e2-900b-54cc8e308f85", + "DisplayName": "M365 License Manager", + "DelegatedPermissions": [ + { + "Id": "fc946a4f-bc4d-413b-a090-b2c86113ec4f", + "Name": "LicenseManager.AccessAsUser", + "Description": "Allows the application to impersonate the signed-in user when communicating with the M365 License Manager service." + } + ], + "ApplicationPermissions": [] + }, + { + "AppId": "00000003-0000-0000-c000-000000000000", + "DisplayName": "Microsoft Graph", + "DelegatedPermissions": [ + { + "Id": "bdfbf15f-ee85-4955-8675-146e8e5296b5", + "Name": "Application.ReadWrite.All", + "Description": "Allows the app to create, read, update and delete applications and service principals on your behalf. Does not allow management of consent grants." + }, + { + "Id": "84bccea3-f856-4a8a-967b-dbe0a3d53a64", + "Name": "AppRoleAssignment.ReadWrite.All", + "Description": "Allows the app to manage permission grants for application permissions to any API (including Microsoft Graph) and application assignments for any app, on your behalf." + }, + { + "Id": "e4c9e354-4dc5-45b8-9e7c-e1393b0b1a20", + "Name": "AuditLog.Read.All", + "Description": "Allows the app to read and query your audit log activities, on your behalf." + }, + { + "Id": "b27a61ec-b99c-4d6a-b126-c4375d08ae30", + "Name": "BitlockerKey.Read.All", + "Description": "Allows the app to read BitLocker keys for your owned devices. Allows read of the recovery key." + }, + { + "Id": "101147cf-4178-4455-9d58-02b5c164e759", + "Name": "Channel.Create", + "Description": "Create channels in any team, on your behalf." + }, + { + "Id": "cc83893a-e232-4723-b5af-bd0b01bcfe65", + "Name": "Channel.Delete.All", + "Description": "Delete channels in any team, on your behalf." + }, + { + "Id": "9d8982ae-4365-4f57-95e9-d6032a4c0b87", + "Name": "Channel.ReadBasic.All", + "Description": "Read channel names and channel descriptions, on your behalf." + }, + { + "Id": "2eadaff8-0bce-4198-a6b9-2cfc35a30075", + "Name": "ChannelMember.Read.All", + "Description": "Read the members of channels, on your behalf." + }, + { + "Id": "0c3e411a-ce45-4cd1-8f30-f99a3efa7b11", + "Name": "ChannelMember.ReadWrite.All", + "Description": "Add and remove members from channels, on your behalf. Also allows changing a member's role, for example from owner to non-owner." + }, + { + "Id": "2b61aa8a-6d36-4b2f-ac7b-f29867937c53", + "Name": "ChannelMessage.Edit", + "Description": "Allows the app to edit channel messages in Microsoft Teams, on your behalf." + }, + { + "Id": "767156cb-16ae-4d10-8f8b-41b657c8c8c8", + "Name": "ChannelMessage.Read.All", + "Description": "Allows the app to read a channel's messages in Microsoft Teams, on your behalf." + }, + { + "Id": "ebf0f66e-9fb1-49e4-a278-222f76911cf4", + "Name": "ChannelMessage.Send", + "Description": "Allows the app to send channel messages in Microsoft Teams, on your behalf." + }, + { + "Id": "233e0cf1-dd62-48bc-b65b-b38fe87fcf8e", + "Name": "ChannelSettings.Read.All", + "Description": "Read all channel names, channel descriptions, and channel settings, on your behalf." + }, + { + "Id": "d649fb7c-72b4-4eec-b2b4-b15acf79e378", + "Name": "ChannelSettings.ReadWrite.All", + "Description": "Read and write the names, descriptions, and settings of all channels, on your behalf." + }, + { + "Id": "f3bfad56-966e-4590-a536-82ecf548ac1e", + "Name": "ConsentRequest.Read.All", + "Description": "Allows the app to read consent requests and approvals, on your behalf." + }, + { + "Id": "885f682f-a990-4bad-a642-36736a74b0c7", + "Name": "DelegatedAdminRelationship.ReadWrite.All", + "Description": "Allows the app to manage (create-update-terminate) Delegated Admin relationships with customers and role assignments to security groups for active Delegated Admin relationships on your behalf." + }, + { + "Id": "41ce6ca6-6826-4807-84f1-1c82854f7ee5", + "Name": "DelegatedPermissionGrant.ReadWrite.All", + "Description": "Allows the app to manage permission grants for delegated permissions exposed by any API (including Microsoft Graph), on your behalf." + }, + { + "Id": "bac3b9c2-b516-4ef4-bd3b-c2ef73d8d804", + "Name": "Device.Command", + "Description": "Allows the app to launch another app or communicate with another app on a device that you own." + }, + { + "Id": "11d4cd79-5ba5-460f-803f-e22c8ab85ccd", + "Name": "Device.Read", + "Description": "Allows the app to see your list of devices." + }, + { + "Id": "951183d1-1a61-466f-a6d1-1fde911bfd95", + "Name": "Device.Read.All", + "Description": "Allows the app to read devices' configuration information on your behalf." + }, + { + "Id": "280b3b69-0437-44b1-bc20-3b2fca1ee3e9", + "Name": "DeviceLocalCredential.Read.All", + "Description": "Allows the app to read device local credential properties including passwords, on your behalf." + }, + { + "Id": "7b3f05d5-f68c-4b8d-8c59-a2ecd12f24af", + "Name": "DeviceManagementApps.ReadWrite.All", + "Description": "Allows the app to read and write the properties, group assignments and status of apps, app configurations and app protection policies managed by Microsoft Intune." + }, + { + "Id": "0883f392-0a7a-443d-8c76-16a6d39c7b63", + "Name": "DeviceManagementConfiguration.ReadWrite.All", + "Description": "Allows the app to read and write properties of Microsoft Intune-managed device configuration and device compliance policies and their assignment to groups." + }, + { + "Id": "3404d2bf-2b13-457e-a330-c24615765193", + "Name": "DeviceManagementManagedDevices.PrivilegedOperations.All", + "Description": "Allows the app to perform remote high impact actions such as wiping the device or resetting the passcode on devices managed by Microsoft Intune." + }, + { + "Id": "44642bfe-8385-4adc-8fc6-fe3cb2c375c3", + "Name": "DeviceManagementManagedDevices.ReadWrite.All", + "Description": "Allows the app to read and write the properties of devices managed by Microsoft Intune. Does not allow high impact operations such as remote wipe and password reset on the device’s owner." + }, + { + "Id": "0c5e8a55-87a6-4556-93ab-adc52c4d862d", + "Name": "DeviceManagementRBAC.ReadWrite.All", + "Description": "Allows the app to read and write the properties relating to the Microsoft Intune Role-Based Access Control (RBAC) settings." + }, + { + "Id": "662ed50a-ac44-4eef-ad86-62eed9be2a29", + "Name": "DeviceManagementServiceConfig.ReadWrite.All", + "Description": "Allows the app to read and write Microsoft Intune service properties including device enrollment and third party service connection configuration." + }, + { + "Id": "0e263e50-5827-48a4-b97c-d940288653c7", + "Name": "Directory.AccessAsUser.All", + "Description": "Allows the app to have the same access to information in your work or school directory as you do." + }, + { + "Id": "c5366453-9fb0-48a5-a156-24f0c49a4b84", + "Name": "Directory.ReadWrite.All", + "Description": "Allows the app to read and write data in your organization's directory, such as other users, groups. It does not allow the app to delete users or groups, or reset user passwords." + }, + { + "Id": "2f9ee017-59c1-4f1d-9472-bd5529a7b311", + "Name": "Domain.Read.All", + "Description": "Allows the app to read all domain properties on your behalf." + }, + { + "Id": "4e46008b-f24c-477d-8fff-7bb4ec7aafe0", + "Name": "Group.ReadWrite.All", + "Description": "Allows the app to create groups and read all group properties and memberships on your behalf. Additionally allows the app to manage your groups and to update group content for groups you are a member of." + }, + { + "Id": "f81125ac-d3b7-4573-a3b2-7099cc39df9e", + "Name": "GroupMember.ReadWrite.All", + "Description": "Allows the app to list groups, read basic properties, read and update the membership of your groups. Group properties and owners cannot be updated and groups cannot be deleted." + }, + { + "Id": "9e4862a5-b68f-479e-848a-4e07e25c9916", + "Name": "IdentityRiskEvent.ReadWrite.All", + "Description": "Allows the app to read and update identity risk event information for all users in your organization on your behalf. Update operations include confirming risk event detections. " + }, + { + "Id": "bb6f654c-d7fd-4ae3-85c3-fc380934f515", + "Name": "IdentityRiskyServicePrincipal.ReadWrite.All", + "Description": "Allows the app to read and update identity risky service principal information for all service principals in your organization, on your behalf. Update operations include dismissing risky service principals." + }, + { + "Id": "e0a7cdbb-08b0-4697-8264-0069786e9674", + "Name": "IdentityRiskyUser.ReadWrite.All", + "Description": "Allows the app to read and update identity risky user information for all users in your organization on your behalf. Update operations include dismissing risky users." + }, + { + "Id": "e383f46e-2787-4529-855e-0e479a3ffac0", + "Name": "Mail.Send", + "Description": "Allows the app to send mail as you." + }, + { + "Id": "a367ab51-6b49-43bf-a716-a1fb06d2a174", + "Name": "Mail.Send.Shared", + "Description": "Allows the app to send mail as you or on-behalf of someone else." + }, + { + "Id": "818c620a-27a9-40bd-a6a5-d96f7d610b4b", + "Name": "MailboxSettings.ReadWrite", + "Description": "Allows the app to read, update, create, and delete your mailbox settings." + }, + { + "Id": "f6a3db3e-f7e8-4ed2-a414-557c8c9830be", + "Name": "Member.Read.Hidden", + "Description": "Allows the app to read the memberships of hidden groups or administrative units on your behalf, for those hidden groups or adminstrative units that you have access to." + }, + { + "Id": "7427e0e9-2fba-42fe-b0c0-848c9e6a8182", + "Name": "offline_access", + "Description": "Allows the app to see and update the data you gave it access to, even when you are not currently using the app. This does not give the app any additional permissions." + }, + { + "Id": "37f7f235-527c-4136-accd-4a02d197296e", + "Name": "openid", + "Description": "Allows you to sign in to the app with your work or school account and allows the app to read your basic profile information." + }, + { + "Id": "46ca0847-7e6b-426e-9775-ea810a948356", + "Name": "Organization.ReadWrite.All", + "Description": "Allows the app to read and write the organization and related resources, on your behalf. Related resources include things like subscribed skus and tenant branding information." + }, + { + "Id": "e67e6727-c080-415e-b521-e3f35d5248e9", + "Name": "PeopleSettings.ReadWrite.All", + "Description": "Allows the application to read and write tenant-wide people settings on your behalf." + }, + { + "Id": "4c06a06a-098a-4063-868e-5dfee3827264", + "Name": "Place.ReadWrite.All", + "Description": "Allows the app to manage organization places (conference rooms and room lists) for calendar events and other applications, on your behalf." + }, + { + "Id": "572fea84-0151-49b2-9301-11cb16974376", + "Name": "Policy.Read.All", + "Description": "Allows the app to read your organization's policies on your behalf." + }, + { + "Id": "b27add92-efb2-4f16-84f5-8108ba77985c", + "Name": "Policy.ReadWrite.ApplicationConfiguration", + "Description": "Allows the app to read and write your organization's application configuration policies on your behalf. This includes policies such as activityBasedTimeoutPolicy, claimsMappingPolicy, homeRealmDiscoveryPolicy, tokenIssuancePolicy and tokenLifetimePolicy." + }, + { + "Id": "edb72de9-4252-4d03-a925-451deef99db7", + "Name": "Policy.ReadWrite.AuthenticationFlows", + "Description": "Allows the app to read and write the authentication flow policies for your tenant, on your behalf." + }, + { + "Id": "7e823077-d88e-468f-a337-e18f1f0e6c7c", + "Name": "Policy.ReadWrite.AuthenticationMethod", + "Description": "Allows the app to read and write the authentication method policies for your tenant, on your behalf." + }, + { + "Id": "edd3c878-b384-41fd-95ad-e7407dd775be", + "Name": "Policy.ReadWrite.Authorization", + "Description": "Allows the app to read and write your organization's authorization policy on your behalf. For example, authorization policies can control some of the permissions that the out-of-the-box user role has by default." + }, + { + "Id": "ad902697-1014-4ef5-81ef-2b4301988e8c", + "Name": "Policy.ReadWrite.ConditionalAccess", + "Description": "Allows the app to read and write your organization's conditional access policies on your behalf." + }, + { + "Id": "4d135e65-66b8-41a8-9f8b-081452c91774", + "Name": "Policy.ReadWrite.ConsentRequest", + "Description": "Allows the app to read and write your organization's consent request policy on your behalf." + }, + { + "Id": "40b534c3-9552-4550-901b-23879c90bcf9", + "Name": "Policy.ReadWrite.DeviceConfiguration", + "Description": "Allows the app to read and write your organization's device configuration policies on your behalf. For example, device registration policy can limit initial provisioning controls using quota restrictions, additional authentication and authorization checks." + }, + { + "Id": "a8ead177-1889-4546-9387-f25e658e2a79", + "Name": "Policy.ReadWrite.MobilityManagement", + "Description": "Allows the app to read and write your organization's mobility management policies on your behalf. For example, a mobility management policy can set the enrollment scope for a given mobility management application." + }, + { + "Id": "1d89d70c-dcac-4248-b214-903c457af83a", + "Name": "PrivilegedAccess.Read.AzureResources", + "Description": "Allows the app to read time-based assignment and just-in-time elevation of Azure resources (like your subscriptions, resource groups, storage, compute) on your behalf." + }, + { + "Id": "a84a9652-ffd3-496e-a991-22ba5529156a", + "Name": "PrivilegedAccess.ReadWrite.AzureResources", + "Description": "Allows the app to request and manage time-based assignment and just-in-time elevation of user privileges to manage  your Azure resources (like your subscriptions, resource groups, storage, compute) on your behalf." + }, + { + "Id": "14dad69e-099b-42c9-810b-d002981feec1", + "Name": "profile", + "Description": "Allows the app to see your basic profile (e.g., name, picture, user name, email address)" + }, + { + "Id": "02e97553-ed7b-43d0-ab3c-f8bace0d040c", + "Name": "Reports.Read.All", + "Description": "Allows an app to read all service usage reports on your behalf. Services that provide usage reports include Office 365 and Azure Active Directory." + }, + { + "Id": "b955410e-7715-4a88-a940-dfd551018df3", + "Name": "ReportSettings.ReadWrite.All", + "Description": "Allows the app to read and update admin report settings, such as whether to display concealed information in reports, on your behalf." + }, + { + "Id": "d01b97e9-cbc0-49fe-810a-750afd5527a3", + "Name": "RoleManagement.ReadWrite.Directory", + "Description": "Allows the app to read and manage the role-based access control (RBAC) settings for your company's directory, on your behalf. This includes instantiating directory roles and managing directory role membership, and reading directory role templates, directory roles and memberships." + }, + { + "Id": "dc38509c-b87d-4da0-bd92-6bec988bac4a", + "Name": "SecurityActions.ReadWrite.All", + "Description": "Allows the app to read and update security actions, on your behalf." + }, + { + "Id": "6aedf524-7e1c-45a7-bd76-ded8cab8d0fc", + "Name": "SecurityEvents.ReadWrite.All", + "Description": "Allows the app to read your organization’s security events on your behalf. Also allows you to update editable properties in security events." + }, + { + "Id": "128ca929-1a19-45e6-a3b8-435ec44a36ba", + "Name": "SecurityIncident.ReadWrite.All", + "Description": "Allows the app to read and write to all security incidents that you have access to." + }, + { + "Id": "55896846-df78-47a7-aa94-8d3d4442ca7f", + "Name": "ServiceHealth.Read.All", + "Description": "Allows the app to read your tenant's service health information on your behalf.Health information may include service issues or service health overviews." + }, + { + "Id": "eda39fa6-f8cf-4c3c-a909-432c683e4c9b", + "Name": "ServiceMessage.Read.All", + "Description": "Allows the app to read your tenant's service announcement messages on your behalf. Messages may include information about new or changed features." + }, + { + "Id": "aa07f155-3612-49b8-a147-6c590df35536", + "Name": "SharePointTenantSettings.ReadWrite.All", + "Description": "Allows the application to read and change the tenant-level settings of SharePoint and OneDrive on your behalf." + }, + { + "Id": "89fe6a52-be36-487e-b7d8-d061c450a026", + "Name": "Sites.ReadWrite.All", + "Description": "Allow the application to edit or delete documents and list items in all site collections on your behalf." + }, + { + "Id": "7825d5d6-6049-4ce7-bdf6-3b8d53f4bcd0", + "Name": "Team.Create", + "Description": "Allows the app to create teams on your behalf. " + }, + { + "Id": "485be79e-c497-4b35-9400-0e3fa7f2a5d4", + "Name": "Team.ReadBasic.All", + "Description": "Read the names and descriptions of teams, on your behalf." + }, + { + "Id": "4a06efd2-f825-4e34-813e-82a57b03d1ee", + "Name": "TeamMember.ReadWrite.All", + "Description": "Add and remove members from teams, on your behalf. Also allows changing a member's role, for example from owner to non-owner." + }, + { + "Id": "2104a4db-3a2f-4ea0-9dba-143d457dc666", + "Name": "TeamMember.ReadWriteNonOwnerRole.All", + "Description": "Add and remove members from all teams, on your behalf. Does not allow adding or removing a member with the owner role. Additionally, does not allow the app to elevate an existing member to the owner role." + }, + { + "Id": "0e755559-83fb-4b44-91d0-4cc721b9323e", + "Name": "TeamsActivity.Read", + "Description": "Allows the app to read your teamwork activity feed." + }, + { + "Id": "48638b3c-ad68-4383-8ac4-e6880ee6ca57", + "Name": "TeamSettings.Read.All", + "Description": "Read all teams' settings, on your behalf." + }, + { + "Id": "39d65650-9d3e-4223-80db-a335590d027e", + "Name": "TeamSettings.ReadWrite.All", + "Description": "Read and change all teams' settings, on your behalf." + }, + { + "Id": "a9ff19c2-f369-4a95-9a25-ba9d460efc8e", + "Name": "TeamsTab.Create", + "Description": "Allows the app to create tabs in any team in Microsoft Teams, on your behalf. This does not grant the ability to read, modify or delete tabs after they are created, or give access to the content inside the tabs." + }, + { + "Id": "b98bfd41-87c6-45cc-b104-e2de4f0dafb9", + "Name": "TeamsTab.ReadWrite.All", + "Description": "Read and write tabs in any team in Microsoft Teams, on your behalf. This does not give access to the content inside the tabs." + }, + { + "Id": "cac97e40-6730-457d-ad8d-4852fddab7ad", + "Name": "ThreatAssessment.ReadWrite.All", + "Description": "Allows an app to read your organization's threat assessment requests on your behalf. Also allows the app to create new requests to assess threats received by your organization on your behalf." + }, + { + "Id": "73e75199-7c3e-41bb-9357-167164dbb415", + "Name": "UnifiedGroupMember.Read.AsGuest", + "Description": "Allows the app to read basic unified group properties, memberships and owners of the group you are a member of." + }, + { + "Id": "637d7bec-b31e-4deb-acc9-24275642a2c9", + "Name": "User.ManageIdentities.All", + "Description": "Allows the app to read, update and delete identities that are associated with a user's account that you have access to. This controls the identities users can sign-in with." + }, + { + "Id": "204e0828-b5ca-4ad8-b9f3-f32a958e7cc4", + "Name": "User.ReadWrite.All", + "Description": "Allows the app to read and write the full set of profile properties, reports, and managers of other users in your organization, on your behalf." + }, + { + "Id": "aec28ec7-4d02-4e8c-b864-50163aea77eb", + "Name": "UserAuthenticationMethod.Read.All", + "Description": "Allows the app to read authentication methods of all users you have access to in your organization. Authentication methods include things like a user’s phone numbers and Authenticator app settings. This does not allow the app to see secret information like passwords, or to sign-in or otherwise use the authentication methods." + }, + { + "Id": "48971fc1-70d7-4245-af77-0beb29b53ee2", + "Name": "UserAuthenticationMethod.ReadWrite", + "Description": "Allows the app to read and write your authentication methods, including phone numbers and Authenticator app settings.This does not allow the app to see secret information like your passwords, or to sign-in or otherwise use your authentication methods." + }, + { + "Id": "b7887744-6746-4312-813d-72daeaee7e2d", + "Name": "UserAuthenticationMethod.ReadWrite.All", + "Description": "Allows the app to read and write authentication methods of all users you have access to in your organization. Authentication methods include things like a user’s phone numbers and Authenticator app settings. This does not allow the app to see secret information like passwords, or to sign-in or otherwise use the authentication methods." + } + ], + "ApplicationPermissions": [ + { + "Id": "1bfefb4e-e0b5-418b-a88f-73c46d2cc8e9", + "Name": "Application.ReadWrite.All", + "Description": "Allows the app to create, read, update and delete applications and service principals without a signed-in user. Does not allow management of consent grants." + }, + { + "Id": "b0afded3-3588-46d8-8b3d-9842eff778da", + "Name": "AuditLog.Read.All", + "Description": "Allows the app to read and query your audit log activities, without a signed-in user." + }, + { + "Id": "5e1e9171-754d-478c-812c-f1755a9a4c2d", + "Name": "AuditLogsQuery.Read.All", + "Description": "Allows the app to read and query audit logs from all services." + }, + { + "Id": "f3a65bd4-b703-46df-8f7e-0174fea562aa", + "Name": "Channel.Create", + "Description": "Create channels in any team, without a signed-in user." + }, + { + "Id": "59a6b24b-4225-4393-8165-ebaec5f55d7a", + "Name": "Channel.ReadBasic.All", + "Description": "Read all channel names and channel descriptions, without a signed-in user." + }, + { + "Id": "3b55498e-47ec-484f-8136-9013221c06a9", + "Name": "ChannelMember.Read.All", + "Description": "Read the members of all channels, without a signed-in user." + }, + { + "Id": "35930dcf-aceb-4bd1-b99a-8ffed403c974", + "Name": "ChannelMember.ReadWrite.All", + "Description": "Add and remove members from all channels, without a signed-in user. Also allows changing a member's role, for example from owner to non-owner." + }, + { + "Id": "cac88765-0581-4025-9725-5ebc13f729ee", + "Name": "CrossTenantInformation.ReadBasic.All", + "Description": "Allows the application to obtain basic tenant information about another target tenant within the Azure AD ecosystem without a signed-in user." + }, + { + "Id": "1138cb37-bd11-4084-a2b7-9f71582aeddb", + "Name": "Device.ReadWrite.All", + "Description": "Allows the app to read and write all device properties without a signed in user. Does not allow device creation, device deletion or update of device alternative security identifiers." + }, + { + "Id": "78145de6-330d-4800-a6ce-494ff2d33d07", + "Name": "DeviceManagementApps.ReadWrite.All", + "Description": "Allows the app to read and write the properties, group assignments and status of apps, app configurations and app protection policies managed by Microsoft Intune, without a signed-in user." + }, + { + "Id": "9241abd9-d0e6-425a-bd4f-47ba86e767a4", + "Name": "DeviceManagementConfiguration.ReadWrite.All", + "Description": "Allows the app to read and write properties of Microsoft Intune-managed device configuration and device compliance policies and their assignment to groups, without a signed-in user." + }, + { + "Id": "5b07b0dd-2377-4e44-a38d-703f09a0dc3c", + "Name": "DeviceManagementManagedDevices.PrivilegedOperations.All", + "Description": "Allows the app to perform remote high impact actions such as wiping the device or resetting the passcode on devices managed by Microsoft Intune, without a signed-in user." + }, + { + "Id": "2f51be20-0bb4-4fed-bf7b-db946066c75e", + "Name": "DeviceManagementManagedDevices.Read.All", + "Description": "Allows the app to read the properties of devices managed by Microsoft Intune, without a signed-in user." + }, + { + "Id": "243333ab-4d21-40cb-a475-36241daa0842", + "Name": "DeviceManagementManagedDevices.ReadWrite.All", + "Description": "Allows the app to read and write the properties of devices managed by Microsoft Intune, without a signed-in user. Does not allow high impact operations such as remote wipe and password reset on the device’s owner" + }, + { + "Id": "58ca0d9a-1575-47e1-a3cb-007ef2e4583b", + "Name": "DeviceManagementRBAC.Read.All", + "Description": "Allows the app to read the properties relating to the Microsoft Intune Role-Based Access Control (RBAC) settings, without a signed-in user." + }, + { + "Id": "e330c4f0-4170-414e-a55a-2f022ec2b57b", + "Name": "DeviceManagementRBAC.ReadWrite.All", + "Description": "Allows the app to read and write the properties relating to the Microsoft Intune Role-Based Access Control (RBAC) settings, without a signed-in user." + }, + { + "Id": "9255e99d-faf5-445e-bbf7-cb71482737c4", + "Name": "DeviceManagementScripts.ReadWrite.All", + "Description": "Allows the app to read and write Microsoft Intune device compliance scripts, device management scripts, device shell scripts, device custom attribute shell scripts and device health scripts, without a signed-in user." + }, + { + "Id": "06a5fe6d-c49d-46a7-b082-56b1b14103c7", + "Name": "DeviceManagementServiceConfig.Read.All", + "Description": "Allows the app to read Microsoft Intune service properties including device enrollment and third party service connection configuration, without a signed-in user." + }, + { + "Id": "5ac13192-7ace-4fcf-b828-1a26f28068ee", + "Name": "DeviceManagementServiceConfig.ReadWrite.All", + "Description": "Allows the app to read and write Microsoft Intune service properties including device enrollment and third party service connection configuration, without a signed-in user." + }, + { + "Id": "7ab1d382-f21e-4acd-a863-ba3e13f7da61", + "Name": "Directory.Read.All", + "Description": "Allows the app to read data in your organization's directory, such as users, groups and apps, without a signed-in user." + }, + { + "Id": "19dbc75e-c2e2-444c-a770-ec69d8559fc7", + "Name": "Directory.ReadWrite.All", + "Description": "Allows the app to read and write data in your organization's directory, such as users, and groups, without a signed-in user. Does not allow user or group deletion." + }, + { + "Id": "dbb9058a-0e50-45d7-ae91-66909b5d4664", + "Name": "Domain.Read.All", + "Description": "Allows the app to read all domain properties without a signed-in user." + }, + { + "Id": "75359482-378d-4052-8f01-80520e7db3cd", + "Name": "Files.ReadWrite.All", + "Description": "Allows the app to read, create, update and delete all files in all site collections without a signed in user." + }, + { + "Id": "bf7b1a76-6e77-406b-b258-bf5c7720e98f", + "Name": "Group.Create", + "Description": "Allows the app to create groups without a signed-in user." + }, + { + "Id": "5b567255-7703-4780-807c-7be8301ae99b", + "Name": "Group.Read.All", + "Description": "Allows the app to read group properties and memberships, and read conversations for all groups, without a signed-in user." + }, + { + "Id": "62a82d76-70ea-41e2-9197-370581804d09", + "Name": "Group.ReadWrite.All", + "Description": "Allows the app to create groups, read all group properties and memberships, update group properties and memberships, and delete groups. Also allows the app to read and write conversations. All of these operations can be performed by the app without a signed-in user." + }, + { + "Id": "dbaae8cf-10b5-4b86-a4a1-f871c94c6695", + "Name": "GroupMember.ReadWrite.All", + "Description": "Allows the app to list groups, read basic properties, read and update the membership of the groups this app has access to without a signed-in user. Group properties and owners cannot be updated and groups cannot be deleted." + }, + { + "Id": "19da66cb-0fb0-4390-b071-ebc76a349482", + "Name": "InformationProtectionPolicy.Read.All", + "Description": "Allows an app to read published sensitivity labels and label policy settings for the entire organization or a specific user, without a signed in user." + }, + { + "Id": "6931bccd-447a-43d1-b442-00a195474933", + "Name": "MailboxSettings.ReadWrite", + "Description": "Allows the app to create, read, update, and delete user's mailbox settings without a signed-in user. Does not include permission to send mail." + }, + { + "Id": "292d869f-3427-49a8-9dab-8c70152b74e9", + "Name": "Organization.ReadWrite.All", + "Description": "Allows the app to read and write the organization and related resources, without a signed-in user. Related resources include things like subscribed skus and tenant branding information." + }, + { + "Id": "b6890674-9dd5-4e42-bb15-5af07f541ae1", + "Name": "PeopleSettings.ReadWrite.All", + "Description": "Allows the application to read and write tenant-wide people settings without a signed-in user." + }, + { + "Id": "913b9306-0ce1-42b8-9137-6a7df690a760", + "Name": "Place.Read.All", + "Description": "Allows the app to read company places (conference rooms and room lists) for calendar events and other applications, without a signed-in user." + }, + { + "Id": "246dd0d5-5bd0-4def-940b-0421030a5b68", + "Name": "Policy.Read.All", + "Description": "Allows the app to read all your organization's policies without a signed in user." + }, + { + "Id": "be74164b-cff1-491c-8741-e671cb536e13", + "Name": "Policy.ReadWrite.ApplicationConfiguration", + "Description": "Allows the app to read and write your organization's application configuration policies, without a signed-in user. This includes policies such as activityBasedTimeoutPolicy, claimsMappingPolicy, homeRealmDiscoveryPolicy, tokenIssuancePolicy and tokenLifetimePolicy." + }, + { + "Id": "25f85f3c-f66c-4205-8cd5-de92dd7f0cec", + "Name": "Policy.ReadWrite.AuthenticationFlows", + "Description": "Allows the app to read and write all authentication flow policies for the tenant, without a signed-in user." + }, + { + "Id": "29c18626-4985-4dcd-85c0-193eef327366", + "Name": "Policy.ReadWrite.AuthenticationMethod", + "Description": "Allows the app to read and write all authentication method policies for the tenant, without a signed-in user. " + }, + { + "Id": "01c0a623-fc9b-48e9-b794-0756f8e8f067", + "Name": "Policy.ReadWrite.ConditionalAccess", + "Description": "Allows the app to read and write your organization's conditional access policies, without a signed-in user." + }, + { + "Id": "999f8c63-0a38-4f1b-91fd-ed1947bdd1a9", + "Name": "Policy.ReadWrite.ConsentRequest", + "Description": "Allows the app to read and write your organization's consent requests policy without a signed-in user." + }, + { + "Id": "338163d7-f101-4c92-94ba-ca46fe52447c", + "Name": "Policy.ReadWrite.CrossTenantAccess", + "Description": "Allows the app to read and write your organization's cross tenant access policies without a signed-in user." + }, + { + "Id": "2f6817f8-7b12-4f0f-bc18-eeaf60705a9e", + "Name": "PrivilegedAccess.ReadWrite.AzureADGroup", + "Description": "Allows the app to request and manage time-based assignment and just-in-time elevation (including scheduled elevation) of Azure AD groups in your organization, without a signed-in user." + }, + { + "Id": "230c1aed-a721-4c5d-9cb4-a90514e508ef", + "Name": "Reports.Read.All", + "Description": "Allows an app to read all service usage reports without a signed-in user. Services that provide usage reports include Office 365 and Azure Active Directory." + }, + { + "Id": "2a60023f-3219-47ad-baa4-40e17cd02a1d", + "Name": "ReportSettings.ReadWrite.All", + "Description": "Allows the app to read and update all admin report settings, such as whether to display concealed information in reports, without a signed-in user." + }, + { + "Id": "04c55753-2244-4c25-87fc-704ab82a4f69", + "Name": "SecurityAnalyzedMessage.ReadWrite.All", + "Description": "Read email metadata and security detection details, and execute remediation actions like deleting an email, without a signed-in user." + }, + { + "Id": "bf394140-e372-4bf9-a898-299cfc7564e5", + "Name": "SecurityEvents.Read.All", + "Description": "Allows the app to read your organization’s security events without a signed-in user." + }, + { + "Id": "45cc0394-e837-488b-a098-1918f48d186c", + "Name": "SecurityIncident.Read.All", + "Description": "Allows the app to read all security incidents, without a signed-in user." + }, + { + "Id": "34bf0e97-1971-4929-b999-9e2442d941d7", + "Name": "SecurityIncident.ReadWrite.All", + "Description": "Allows the app to read and write to all security incidents, without a signed-in user." + }, + { + "Id": "19b94e34-907c-4f43-bde9-38b1909ed408", + "Name": "SharePointTenantSettings.ReadWrite.All", + "Description": "Allows the application to read and change the tenant-level settings of SharePoint and OneDrive, without a signed-in user." + }, + { + "Id": "a82116e5-55eb-4c41-a434-62fe8a61c773", + "Name": "Sites.FullControl.All", + "Description": "Allows the app to have full control of all site collections without a signed in user." + }, + { + "Id": "0121dc95-1b9f-4aed-8bac-58c5ac466691", + "Name": "TeamMember.ReadWrite.All", + "Description": "Add and remove members from all teams, without a signed-in user. Also allows changing a team member's role, for example from owner to non-owner." + }, + { + "Id": "4437522e-9a86-4a41-a7da-e380edd4a97d", + "Name": "TeamMember.ReadWriteNonOwnerRole.All", + "Description": "Add and remove members from all teams, without a signed-in user. Does not allow adding or removing a member with the owner role. Additionally, does not allow the app to elevate an existing member to the owner role." + }, + { + "Id": "741f803b-c850-494e-b5df-cde7c675a1ca", + "Name": "User.ReadWrite.All", + "Description": "Allows the app to read and update user profiles without a signed in user." + }, + { + "Id": "50483e42-d915-4231-9639-7fdb7fd190e5", + "Name": "UserAuthenticationMethod.ReadWrite.All", + "Description": "Allows the application to read and write authentication methods of all users in your organization, without a signed-in user. Authentication methods include things like a user’s phone numbers and Authenticator app settings. This does not allow the app to see secret information like passwords, or to sign-in or otherwise use the authentication methods" + } + ] + }, + { + "AppId": "fa3d9a0c-3fb0-42cc-9193-47c7ecd2edbd", + "DisplayName": "Microsoft Partner Center", + "DelegatedPermissions": [ + { + "Id": "1cebfa2a-fb4d-419e-b5f9-839b4383e05a", + "Name": "user_impersonation", + "Description": "Allow the application to access Partner Center on your behalf" + } + ], + "ApplicationPermissions": [] + }, + { + "AppId": "00000002-0000-0ff1-ce00-000000000000", + "DisplayName": "Office 365 Exchange Online", + "DelegatedPermissions": [ + { + "Id": "ab4f2b77-0b06-4fc1-a9de-02113fc2ab7c", + "Name": "Exchange.Manage", + "Description": "Allows the app to manage your organization's Exchange environment, such as mailboxes, groups, and other configuration objects. To enable management actions, an admin must assign you the appropriate roles." + }, + { + "Id": "bbd1ca91-75e0-4814-ad94-9c5dbbae3415", + "Name": "Calendars.ReadWrite.All", + "Description": "Allows the app to read, update, create and delete events in all calendars in your organization you have permissions to access. This includes delegate and shared calendars. " + }, + { + "Id": "2e83d72d-8895-4b66-9eea-abb43449ab8b", + "Name": "MailboxSettings.ReadWrite", + "Description": "Allows the app to read, update, create, and delete your mailbox settings." + } + ], + "ApplicationPermissions": [ + { + "Id": "dc50a0fb-09a3-484d-be87-e023b12c6440", + "Name": "Exchange.ManageAsApp", + "Description": "Allows the app to manage the organization's Exchange environment without any user interaction. This includes mailboxes, groups, and other configuration objects. To enable management actions, an admin must assign the appropriate roles directly to the app." + }, + { + "Id": "ef54d2bf-783f-4e0f-bca1-3210c0444d99", + "Name": "Calendars.ReadWrite.All", + "Description": "Allows the app to create, read, update, and delete events of all calendars without a signed-in user." + }, + { + "Id": "f9156939-25cd-4ba8-abfe-7fabcf003749", + "Name": "MailboxSettings.ReadWrite", + "Description": "Allows the app to create, read, update, and delete user's mailbox settings without a signed-in user. Does not include permission to send mail." + } + ] + }, + { + "AppId": "00000003-0000-0ff1-ce00-000000000000", + "DisplayName": "Office 365 SharePoint Online", + "DelegatedPermissions": [ + { + "Id": "56680e0d-d2a3-4ae1-80d8-3c4f2100e3d0", + "Name": "AllSites.FullControl", + "Description": "Allows the app to have full control of all site collections on your behalf." + }, + { + "Id": "AllProfiles.Manage", + "Name": "AllProfiles.Manage", + "Description": "Manually added" + } + ], + "ApplicationPermissions": [] + }, + { + "AppId": "48ac35b8-9aa8-4d74-927d-1f4a14a0b239", + "DisplayName": "Skype and Teams Tenant Admin API", + "DelegatedPermissions": [ + { + "Id": "e60370c1-e451-437e-aa6e-d76df38e5f15", + "Name": "user_impersonation", + "Description": "Access Microsoft Teams and Skype for Business data based on the user's role membership" + } + ], + "ApplicationPermissions": [] + }, + { + "AppId": "fc780465-2017-40d4-a0c5-307022471b92", + "DisplayName": "WindowsDefenderATP", + "DelegatedPermissions": [ + { + "Id": "63a677ce-818c-4409-9d12-5c6d2e2a6bfe", + "Name": "Vulnerability.Read", + "Description": "Allows the app to read Threat and Vulnerability Management vulnerability information on behalf of the signed-in user" + } + ], + "ApplicationPermissions": [ + { + "Id": "41269fc5-d04d-4bfd-bce7-43a51cea049a", + "Name": "Vulnerability.Read.All", + "Description": "Allows the app to read any Threat and Vulnerability Management vulnerability information" + } + ] + } +] From 9f36283f24b62c0f4cafb751e9e96cae4badd7df Mon Sep 17 00:00:00 2001 From: Esco Date: Thu, 19 Jun 2025 13:25:42 +0200 Subject: [PATCH 128/160] fix: fix template application deployment --- .../Public/Standards/Invoke-CIPPStandardAppDeploy.ps1 | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAppDeploy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAppDeploy.ps1 index fb768688f7de..f4c51a1a7048 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAppDeploy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAppDeploy.ps1 @@ -69,15 +69,15 @@ function Invoke-CIPPStandardAppDeploy { foreach ($AppId in $AppIds) { if ($AppId -notin $AppExists.appId) { - Write-Information "Adding $($AppId) to tenant $($Tenant)." - $PostResults = New-GraphPostRequest 'https://graph.microsoft.com/beta/servicePrincipals' -type POST -tenantid $Item.tenant -body "{ `"appId`": `"$($Item.appId)`" }" - Write-LogMessage -message "Added $($Item.AppId) to tenant $($Item.Tenant)" -tenant $Item.Tenant -API 'Add Multitenant App' -sev Info + Write-Information "Adding $AppId to tenant $Tenant." + $PostResults = New-GraphPostRequest 'https://graph.microsoft.com/beta/servicePrincipals' -type POST -tenantid $Tenant -body "{ `"appId`": `"$AppId`" }" + Write-LogMessage -message "Added $AppId to tenant $Tenant" -tenant $Tenant -API 'Add Multitenant App' -sev Info } } foreach ($TemplateId in $TemplateIds) { try { - Add-CIPPApplicationPermission -TemplateId $TemplateId -Tenantfilter $Tenant - Add-CIPPDelegatedPermission -TemplateId $TemplateId -Tenantfilter $Tenant + Add-CIPPApplicationPermission -TemplateId $TemplateId -TenantFilter $Tenant + Add-CIPPDelegatedPermission -TemplateId $TemplateId -TenantFilter $Tenant Write-LogMessage -API 'Standards' -tenant $tenant -message "Added application(s) from template $($TemplateName) and updated it's permissions" -sev Info } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message From f4ec86c3ac9ce4e878f36b6d8737a92d995b439b Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 19 Jun 2025 13:42:02 +0200 Subject: [PATCH 129/160] new standard --- ...tandardDisableExchangeOnlinePowerShell.ps1 | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExchangeOnlinePowerShell.ps1 diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExchangeOnlinePowerShell.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExchangeOnlinePowerShell.ps1 new file mode 100644 index 000000000000..33b0ab22ed63 --- /dev/null +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExchangeOnlinePowerShell.ps1 @@ -0,0 +1,95 @@ +function Invoke-CIPPStandardDisableExchangeOnlinePowerShell { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) DisableExchangeOnlinePowerShell + .SYNOPSIS + (Label) Disable Exchange Online PowerShell for non-admin users + .DESCRIPTION + (Helptext) Disables the ability for non-admin users to use Exchange Online PowerShell. Only administrators will be able to use PowerShell to connect to Exchange Online. + (DocsDescription) Disables the ability for non-admin users to use Exchange Online PowerShell. This helps prevent attackers from using PowerShell to run malicious commands, access file systems, registry, and distribute ransomware throughout networks. Only administrators will be able to use PowerShell to connect to Exchange Online, aligning with a least privileged access approach to security. + .NOTES + CAT + Exchange Standards + TAG + "CIS" + "PowerShell" + "Security" + ADDEDCOMPONENT + IMPACT + Medium Impact + ADDEDDATE + 2025-06-19 + POWERSHELLEQUIVALENT + Get-User -ResultSize Unlimited -Filter 'RemotePowerShellEnabled -eq $true' | ForEach-Object { Set-User -Identity $_.Identity -RemotePowerShellEnabled $false } + RECOMMENDEDBY + "CIS" + "CIPP" + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + .LINK + https://docs.cipp.app/user-documentation/tenant/standards/list-standards + #> + + param($Tenant, $Settings) + ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'DisableExchangeOnlinePowerShell' + + try { + + $AdminUsers = (New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments?$expand=principal' -tenantid $Tenant).principal.userPrincipalName + $UsersWithPowerShell = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-User' -Select 'userPrincipalName, identity, remotePowerShellEnabled' | Where-Object { $_.RemotePowerShellEnabled -eq $true -and $_.userPrincipalName -notin $AdminUsers } + $PowerShellEnabledCount = ($UsersWithPowerShell | Measure-Object).Count + $StateIsCorrect = $PowerShellEnabledCount -eq 0 + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $tenant -message "Could not check Exchange Online PowerShell status. $($ErrorMessage.NormalizedError)" -sev Error + $StateIsCorrect = $null + } + + if ($Settings.remediate -eq $true) { + if ($PowerShellEnabledCount -gt 0) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Started disabling Exchange Online PowerShell for $PowerShellEnabledCount users." -sev Info + + $Request = $UsersWithPowerShell | ForEach-Object { + @{ + CmdletInput = @{ + CmdletName = 'Set-User' + Parameters = @{Identity = $_.Identity; RemotePowerShellEnabled = $false } + } + } + } + + $BatchResults = New-ExoBulkRequest -tenantid $tenant -cmdletArray @($Request) + $SuccessCount = 0 + $BatchResults | ForEach-Object { + if ($_.error) { + $ErrorMessage = Get-NormalizedError -Message $_.error + Write-Host "Failed to disable Exchange Online PowerShell for $($_.target). Error: $ErrorMessage" + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to disable Exchange Online PowerShell for $($_.target). Error: $ErrorMessage" -sev Error + } else { + $SuccessCount++ + } + } + + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully disabled Exchange Online PowerShell for $SuccessCount out of $PowerShellEnabledCount users." -sev Info + } else { + Write-LogMessage -API 'Standards' -tenant $tenant -message 'Exchange Online PowerShell is already disabled for all non-admin users' -sev Info + } + } + + if ($Settings.alert -eq $true) { + if ($StateIsCorrect -eq $true) { + Write-LogMessage -API 'Standards' -tenant $tenant -message 'Exchange Online PowerShell is disabled for all non-admin users.' -sev Info + } else { + Write-StandardsAlert -message "Exchange Online PowerShell is enabled for $PowerShellEnabledCount users" -object @{UsersWithPowerShellEnabled = $PowerShellEnabledCount } -tenant $tenant -standardName 'DisableExchangeOnlinePowerShell' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $tenant -message "Exchange Online PowerShell is enabled for $PowerShellEnabledCount users." -sev Info + } + } + + if ($Settings.report -eq $true) { + $state = $StateIsCorrect ?? @{UsersWithPowerShellEnabled = $PowerShellEnabledCount } + Set-CIPPStandardsCompareField -FieldName 'standards.DisableExchangeOnlinePowerShell' -FieldValue $state -TenantFilter $Tenant + Add-CIPPBPAField -FieldName 'ExchangeOnlinePowerShellDisabled' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant + } +} From 280654ef4095b17af13821352d0b10f6c4166578 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Thu, 19 Jun 2025 15:24:05 +0200 Subject: [PATCH 130/160] SharePoint Admin URL lookup simplification --- .../Alerts/Get-CIPPAlertSharepointQuota.ps1 | 9 +-- .../Invoke-ListSharepointAdminUrl.ps1 | 5 +- .../Invoke-ListSharepointQuota.ps1 | 8 +-- Modules/CIPPCore/Public/Get-CIPPSPOTenant.ps1 | 8 ++- .../GraphHelper/Get-SharePointAdminLink.ps1 | 68 +++++++++++++++++++ .../Public/New-CIPPSharepointSite.ps1 | 11 ++- .../Public/Request-CIPPSPOPersonalSite.ps1 | 6 +- Modules/CIPPCore/Public/Set-CIPPSPOTenant.ps1 | 5 +- .../Public/Set-CIPPSharePointPerms.ps1 | 7 +- 9 files changed, 94 insertions(+), 33 deletions(-) create mode 100644 Modules/CIPPCore/Public/GraphHelper/Get-SharePointAdminLink.ps1 diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSharepointQuota.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSharepointQuota.ps1 index ed3ed22e4b95..2460a77905f5 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSharepointQuota.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSharepointQuota.ps1 @@ -1,4 +1,3 @@ - function Get-CIPPAlertSharepointQuota { <# .FUNCTIONALITY @@ -12,10 +11,8 @@ function Get-CIPPAlertSharepointQuota { $TenantFilter ) Try { - $tenantName = (New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/sites/root' -asApp $true -tenantid $TenantFilter).id.Split('.')[0] - $sharepointToken = (Get-GraphToken -scope "https://$($tenantName)-admin.sharepoint.com/.default" -tenantid $TenantFilter) - $sharepointToken.Add('accept', 'application/json') - $sharepointQuota = (Invoke-RestMethod -Method 'GET' -Headers $sharepointToken -Uri "https://$($tenantName)-admin.sharepoint.com/_api/StorageQuotas()?api-version=1.3.2" -ErrorAction Stop).value + $SharePointInfo = Get-SharePointAdminLink -Public $false + $sharepointQuota = (New-GraphGetRequest -scope "$($SharePointInfo.AdminUrl)/.default" -tenantid $TenantFilter -uri "$($SharePointInfo.AdminUrl)/_api/StorageQuotas()?api-version=1.3.2").value } catch { return } @@ -31,4 +28,4 @@ function Get-CIPPAlertSharepointQuota { Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData } } -} \ No newline at end of file +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointAdminUrl.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointAdminUrl.ps1 index ef0e1e5c690e..31bf6a2aabf6 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointAdminUrl.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointAdminUrl.ps1 @@ -19,9 +19,8 @@ function Invoke-ListSharepointAdminUrl { if ($Tenant.SharepointAdminUrl) { $AdminUrl = $Tenant.SharepointAdminUrl } else { - $tenantName = (New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/sites/root' -asApp $true -tenantid $TenantFilter).id.Split('.')[0] - $AdminUrl = "https://$($tenantName)-admin.sharepoint.com" - $Tenant | Add-Member -MemberType NoteProperty -Name SharepointAdminUrl -Value $AdminUrl + $SharePointInfo = Get-SharePointAdminLink -Public $false + $Tenant | Add-Member -MemberType NoteProperty -Name SharepointAdminUrl -Value $SharePointInfo.AdminUrl $Table = Get-CIPPTable -TableName 'Tenants' Add-CIPPAzDataTableEntity @Table -Entity $Tenant -Force } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointQuota.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointQuota.ps1 index d32656423abb..c2ca4eea4135 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointQuota.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointQuota.ps1 @@ -21,12 +21,8 @@ Function Invoke-ListSharepointQuota { $UsedStoragePercentage = 'Not Supported' } else { try { - $TenantName = (New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/sites/root' -asApp $true -tenantid $TenantFilter).id.Split('.')[0] - - $SharePointToken = (Get-GraphToken -scope "https://$($TenantName)-admin.sharepoint.com/.default" -tenantid $TenantFilter) - $SharePointToken.Add('accept', 'application/json') - # Implement a try catch later to deal with SharePoint guest user settings - $SharePointQuota = (Invoke-RestMethod -Method 'GET' -Headers $SharePointToken -Uri "https://$($TenantName)-admin.sharepoint.com/_api/StorageQuotas()?api-version=1.3.2" -ErrorAction Stop).value | Sort-Object -Property GeoUsedStorageMB -Descending | Select-Object -First 1 + $SharePointInfo = Get-SharePointAdminLink -Public $false + $SharePointQuota = (New-GraphGetRequest -scope "$($SharePointInfo.AdminUrl)/.default" -tenantid $TenantFilter -uri "$($SharePointInfo.AdminUrl)/_api/StorageQuotas()?api-version=1.3.2").value | Sort-Object -Property GeoUsedStorageMB -Descending | Select-Object -First 1 if ($SharePointQuota) { $UsedStoragePercentage = [int](($SharePointQuota.GeoUsedStorageMB / $SharePointQuota.TenantStorageMB) * 100) diff --git a/Modules/CIPPCore/Public/Get-CIPPSPOTenant.ps1 b/Modules/CIPPCore/Public/Get-CIPPSPOTenant.ps1 index fec489bc729d..16efdfa588ed 100644 --- a/Modules/CIPPCore/Public/Get-CIPPSPOTenant.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPSPOTenant.ps1 @@ -8,11 +8,13 @@ function Get-CIPPSPOTenant { if (!$SharepointPrefix) { # get sharepoint admin site - $tenantName = (New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/sites/root' -asApp $true -tenantid $TenantFilter).id.Split('.')[0] + $SharePointInfo = Get-SharePointAdminLink -Public $false + $tenantName = $SharePointInfo.TenantName + $AdminUrl = $SharePointInfo.AdminUrl } else { $tenantName = $SharepointPrefix + $AdminUrl = "https://$($tenantName)-admin.sharepoint.com" } - $AdminUrl = "https://$($tenantName)-admin.sharepoint.com" # Query tenant settings $XML = @' @@ -21,7 +23,7 @@ function Get-CIPPSPOTenant { $AdditionalHeaders = @{ 'Accept' = 'application/json;odata=verbose' } - $Results = New-GraphPostRequest -scope "$AdminURL/.default" -tenantid $TenantFilter -Uri "$AdminURL/_vti_bin/client.svc/ProcessQuery" -Type POST -Body $XML -ContentType 'text/xml' -AddedHeaders $AdditionalHeaders + $Results = New-GraphPostRequest -scope "$($AdminUrl)/.default" -tenantid $TenantFilter -Uri "$($SharePointInfo.AdminUrl)/_vti_bin/client.svc/ProcessQuery" -Type POST -Body $XML -ContentType 'text/xml' -AddedHeaders $AdditionalHeaders $Results | Select-Object -Last 1 *, @{n = 'SharepointPrefix'; e = { $tenantName } }, @{n = 'TenantFilter'; e = { $TenantFilter } } } diff --git a/Modules/CIPPCore/Public/GraphHelper/Get-SharePointAdminLink.ps1 b/Modules/CIPPCore/Public/GraphHelper/Get-SharePointAdminLink.ps1 new file mode 100644 index 000000000000..9e471ec8503e --- /dev/null +++ b/Modules/CIPPCore/Public/GraphHelper/Get-SharePointAdminLink.ps1 @@ -0,0 +1,68 @@ +function Get-SharePointAdminLink { + <# + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param ($Public) + + if ($Public) { + # Do it through domain discovery, unreliable + try { + # Get tenant information using autodiscover + $body = @" + + + + http://schemas.microsoft.com/exchange/2010/Autodiscover/Autodiscover/GetFederationInformation + https://autodiscover-s.outlook.com/autodiscover/autodiscover.svc + + http://www.w3.org/2005/08/addressing/anonymous + + + + + + $TenantFilter + + + + +"@ + + # Create the headers + $AutoDiscoverHeaders = @{ + 'Content-Type' = 'text/xml; charset=utf-8' + 'SOAPAction' = '"http://schemas.microsoft.com/exchange/2010/Autodiscover/Autodiscover/GetFederationInformation"' + 'User-Agent' = 'AutodiscoverClient' + } + + # Invoke autodiscover + $Response = Invoke-RestMethod -UseBasicParsing -Method Post -Uri 'https://autodiscover-s.outlook.com/autodiscover/autodiscover.svc' -Body $body -Headers $AutoDiscoverHeaders + + # Get the onmicrosoft.com domain from the response + $TenantDomains = $Response.Envelope.body.GetFederationInformationResponseMessage.response.Domains.Domain | Sort-Object + $OnMicrosoftDomains = $TenantDomains | Where-Object { $_ -like "*.onmicrosoft.com" } + + if ($OnMicrosoftDomains.Count -eq 0) { + throw "Could not find onmicrosoft.com domain through autodiscover" + } elseif ($OnMicrosoftDomains.Count -gt 1) { + throw "Multiple onmicrosoft.com domains found through autodiscover. Cannot determine the correct one: $($OnMicrosoftDomains -join ', ')" + } else { + $OnMicrosoftDomain = $OnMicrosoftDomains[0] + $tenantName = $OnMicrosoftDomain.Split('.')[0] + } + } catch { + throw "Failed to get SharePoint admin URL through autodiscover: $($_.Exception.Message)" + } + } else { + $tenantName = (New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/sites/root' -asApp $true -tenantid $TenantFilter).id.Split('.')[0] + } + + # Return object with all needed properties + return [PSCustomObject]@{ + AdminUrl = "https://$tenantName-admin.sharepoint.com" + TenantName = $tenantName + SharePointUrl = "https://$tenantName.sharepoint.com" + } +} \ No newline at end of file diff --git a/Modules/CIPPCore/Public/New-CIPPSharepointSite.ps1 b/Modules/CIPPCore/Public/New-CIPPSharepointSite.ps1 index 6a37dbbf088a..7c1074e4241d 100644 --- a/Modules/CIPPCore/Public/New-CIPPSharepointSite.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPSharepointSite.ps1 @@ -65,13 +65,10 @@ function New-CIPPSharepointSite { $APIName = 'Create SharePoint Site', $Headers ) - $tenantName = (New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/sites/root' -asApp $true -tenantid $TenantFilter).id.Split('.')[0] - $AdminUrl = "https://$($tenantName)-admin.sharepoint.com" - $SitePath = $SiteName -replace ' ' -replace '[^A-Za-z0-9-]' - $SiteUrl = "https://$tenantName.sharepoint.com/sites/$SitePath" - - + $SharePointInfo = Get-SharePointAdminLink -Public $false + $SitePath = $SiteName -replace ' ' -replace '[^A-Za-z0-9-]' + $SiteUrl = "https://$($SharePointInfo.TenantName).sharepoint.com/sites/$SitePath" switch ($TemplateName) { 'Communication' { @@ -142,7 +139,7 @@ function New-CIPPSharepointSite { 'accept' = 'application/json;odata.metadata=none' 'odata-version' = '4.0' } - $Results = New-GraphPostRequest -scope "$AdminUrl/.default" -uri "$AdminUrl/_api/SPSiteManager/create" -Body ($body | ConvertTo-Json -Compress -Depth 10) -tenantid $TenantFilter -ContentType 'application/json' -AddedHeaders $AddedHeaders + $Results = New-GraphPostRequest -scope "$($SharePointInfo.AdminUrl)/.default" -uri "$($SharePointInfo.AdminUrl)/_api/SPSiteManager/create" -Body ($body | ConvertTo-Json -Compress -Depth 10) -tenantid $TenantFilter -ContentType 'application/json' -AddedHeaders $AddedHeaders } # Check the results. This response is weird. https://learn.microsoft.com/en-us/sharepoint/dev/apis/site-creation-rest diff --git a/Modules/CIPPCore/Public/Request-CIPPSPOPersonalSite.ps1 b/Modules/CIPPCore/Public/Request-CIPPSPOPersonalSite.ps1 index d3707829cc0f..dfee53d3e9c9 100644 --- a/Modules/CIPPCore/Public/Request-CIPPSPOPersonalSite.ps1 +++ b/Modules/CIPPCore/Public/Request-CIPPSPOPersonalSite.ps1 @@ -36,11 +36,11 @@ function Request-CIPPSPOPersonalSite { "@ - $tenantName = (New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/sites/root' -asApp $true -tenantid $TenantFilter).id.Split('.')[0] - $AdminUrl = "https://$($tenantName)-admin.sharepoint.com" + + $SharePointInfo = Get-SharePointAdminLink -Public $false try { - $Request = New-GraphPostRequest -scope "$AdminURL/.default" -tenantid $TenantFilter -Uri "$AdminURL/_vti_bin/client.svc/ProcessQuery" -Type POST -Body $XML -ContentType 'text/xml' + $Request = New-GraphPostRequest -scope "$($SharePointInfo.AdminUrl)/.default" -tenantid $TenantFilter -Uri "$($SharePointInfo.AdminUrl)/_vti_bin/client.svc/ProcessQuery" -Type POST -Body $XML -ContentType 'text/xml' if (!$Request.IsComplete) { throw } Write-LogMessage -headers $Headers -API $APIName -message "Requested personal site for $($UserEmails -join ', ')" -Sev 'Info' -tenant $TenantFilter return "Successfully requested personal site for $($UserEmails -join ', ')" diff --git a/Modules/CIPPCore/Public/Set-CIPPSPOTenant.ps1 b/Modules/CIPPCore/Public/Set-CIPPSPOTenant.ps1 index 620936d895fb..57732fbbfdb1 100644 --- a/Modules/CIPPCore/Public/Set-CIPPSPOTenant.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPSPOTenant.ps1 @@ -44,12 +44,13 @@ function Set-CIPPSPOTenant { process { if (!$SharepointPrefix) { # get sharepoint admin site - $tenantName = (New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/sites/root' -asApp $true -tenantid $TenantFilter).id.Split('.')[0] + $SharePointInfo = Get-SharePointAdminLink -Public $false + $AdminUrl = $SharePointInfo.AdminUrl } else { $tenantName = $SharepointPrefix + $AdminUrl = "https://$($tenantName)-admin.sharepoint.com" } $Identity = $Identity -replace "`n", ' ' - $AdminUrl = "https://$($tenantName)-admin.sharepoint.com" $AllowedTypes = @('Boolean', 'String', 'Int32') $SetProperty = [System.Collections.Generic.List[string]]::new() $x = 114 diff --git a/Modules/CIPPCore/Public/Set-CIPPSharePointPerms.ps1 b/Modules/CIPPCore/Public/Set-CIPPSharePointPerms.ps1 index 66a87747b371..060dbd8076ca 100644 --- a/Modules/CIPPCore/Public/Set-CIPPSharePointPerms.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPSharePointPerms.ps1 @@ -20,8 +20,9 @@ function Set-CIPPSharePointPerms { Write-Information 'No URL provided, getting URL from Graph' $URL = (New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/users/$($UserId)/Drives" -asapp $true -tenantid $TenantFilter).WebUrl } - $tenantName = (New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/sites/root' -asApp $true -tenantid $TenantFilter).id.Split('.')[0] - $AdminUrl = "https://$($tenantName)-admin.sharepoint.com" + + $SharePointInfo = Get-SharePointAdminLink -Public $false + $XML = @" @@ -39,7 +40,7 @@ function Set-CIPPSharePointPerms { "@ - $request = New-GraphPostRequest -scope "$AdminURL/.default" -tenantid $TenantFilter -Uri "$AdminURL/_vti_bin/client.svc/ProcessQuery" -Type POST -Body $XML -ContentType 'text/xml' + $request = New-GraphPostRequest -scope "$($SharePointInfo.AdminUrl)/.default" -tenantid $TenantFilter -Uri "$($SharePointInfo.AdminUrl)/_vti_bin/client.svc/ProcessQuery" -Type POST -Body $XML -ContentType 'text/xml' # Write-Host $($request) if (!$request.ErrorInfo.ErrorMessage) { $Message = "$($OnedriveAccessUser) has been $($RemovePermission ? 'removed from' : 'given') access to $URL" From 484854cce9e124611cb6e7aa0329534bca7966b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Thu, 19 Jun 2025 17:40:33 +0200 Subject: [PATCH 131/160] dont always return a success as that breaks resetForm=true --- .../Administration/Groups/Invoke-AddGroup.ps1 | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-AddGroup.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-AddGroup.ps1 index e17056f91419..8a14f5d0928a 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-AddGroup.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-AddGroup.ps1 @@ -36,14 +36,12 @@ function Invoke-AddGroup { if ($GroupObject.groupType -eq 'm365') { $BodyParams | Add-Member -NotePropertyName 'groupTypes' -NotePropertyValue @('Unified', 'DynamicMembership') $BodyParams.mailEnabled = $true - } - else { + } else { $BodyParams | Add-Member -NotePropertyName 'groupTypes' -NotePropertyValue @('DynamicMembership') } # Skip adding static members if we're using dynamic membership $SkipStaticMembers = $true - } - elseif ($GroupObject.groupType -eq 'm365') { + } elseif ($GroupObject.groupType -eq 'm365') { $BodyParams | Add-Member -NotePropertyName 'groupTypes' -NotePropertyValue @('Unified') $BodyParams.mailEnabled = $true } @@ -56,8 +54,7 @@ function Invoke-AddGroup { $BodyParams.'members@odata.bind' = @($BodyParams.'members@odata.bind') } $GraphRequest = New-GraphPostRequest -uri 'https://graph.microsoft.com/beta/groups' -tenantid $tenant -type POST -body (ConvertTo-Json -InputObject $BodyParams -Depth 10) -Verbose - } - else { + } else { if ($GroupObject.groupType -eq 'dynamicDistribution') { $ExoParams = @{ Name = $GroupObject.displayName @@ -65,8 +62,7 @@ function Invoke-AddGroup { PrimarySmtpAddress = $Email } $GraphRequest = New-ExoRequest -tenantid $tenant -cmdlet 'New-DynamicDistributionGroup' -cmdParams $ExoParams - } - else { + } else { $ExoParams = @{ Name = $GroupObject.displayName Alias = $GroupObject.username @@ -87,19 +83,18 @@ function Invoke-AddGroup { "Successfully created group $($GroupObject.displayName) for $($tenant)" Write-LogMessage -headers $Request.Headers -API $APIName -tenant $tenant -message "Created group $($GroupObject.displayName) with id $($GraphRequest.id)" -Sev Info - - } - catch { + $StatusCode = [HttpStatusCode]::OK + } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -headers $Request.Headers -API $APIName -tenant $tenant -message "Group creation API failed. $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage "Failed to create group. $($GroupObject.displayName) for $($tenant) $($ErrorMessage.NormalizedError)" + $StatusCode = [HttpStatusCode]::InternalServerError } } - $ResponseBody = [pscustomobject]@{'Results' = @($Results) } # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ - StatusCode = [HttpStatusCode]::OK - Body = $ResponseBody + StatusCode = $StatusCode + Body = @{'Results' = @($Results) } }) } From c57970a7de8e570ab903588d36684a5c2dfbe32b Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 19 Jun 2025 18:58:14 +0200 Subject: [PATCH 132/160] force add of report to every remediate standard. --- .../Public/Standards/Get-CIPPStandards.ps1 | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/Modules/CIPPCore/Public/Standards/Get-CIPPStandards.ps1 b/Modules/CIPPCore/Public/Standards/Get-CIPPStandards.ps1 index 31643c813caf..7eaf01a80d1e 100644 --- a/Modules/CIPPCore/Public/Standards/Get-CIPPStandards.ps1 +++ b/Modules/CIPPCore/Public/Standards/Get-CIPPStandards.ps1 @@ -60,6 +60,14 @@ function Get-CIPPStandards { $CurrentStandard = $Item.PSObject.Copy() $CurrentStandard | Add-Member -NotePropertyName 'TemplateId' -NotePropertyValue $Template.GUID -Force + if ($CurrentStandard.action.value -contains 'Remediate' -and -not ($CurrentStandard.action.value -contains 'Report')) { + $reportAction = [pscustomobject]@{ + label = 'Report' + value = 'Report' + } + $CurrentStandard.action = @($CurrentStandard.action) + $reportAction + } + $Actions = $CurrentStandard.action.value if ($Actions -contains 'Remediate' -or $Actions -contains 'warn' -or $Actions -contains 'Report') { if (-not $ComputedStandards.Contains($StandardName)) { @@ -75,6 +83,14 @@ function Get-CIPPStandards { $CurrentStandard = $Value.PSObject.Copy() $CurrentStandard | Add-Member -NotePropertyName 'TemplateId' -NotePropertyValue $Template.GUID -Force + if ($CurrentStandard.action.value -contains 'Remediate' -and -not ($CurrentStandard.action.value -contains 'Report')) { + $reportAction = [pscustomobject]@{ + label = 'Report' + value = 'Report' + } + $CurrentStandard.action = @($CurrentStandard.action) + $reportAction + } + $Actions = $CurrentStandard.action.value if ($Actions -contains 'Remediate' -or $Actions -contains 'warn' -or $Actions -contains 'Report') { if (-not $ComputedStandards.Contains($StandardName)) { @@ -190,6 +206,14 @@ function Get-CIPPStandards { $CurrentStandard = $Item.PSObject.Copy() $CurrentStandard | Add-Member -NotePropertyName 'TemplateId' -NotePropertyValue $Template.GUID -Force + if ($CurrentStandard.action.value -contains 'Remediate' -and -not ($CurrentStandard.action.value -contains 'Report')) { + $reportAction = [pscustomobject]@{ + label = 'Report' + value = 'Report' + } + $CurrentStandard.action = @($CurrentStandard.action) + $reportAction + } + $Actions = $CurrentStandard.action.value if ($Actions -contains 'Remediate' -or $Actions -contains 'warn' -or $Actions -contains 'Report') { if (-not $ComputedStandards.Contains($StandardName)) { @@ -204,6 +228,14 @@ function Get-CIPPStandards { $CurrentStandard = $Value.PSObject.Copy() $CurrentStandard | Add-Member -NotePropertyName 'TemplateId' -NotePropertyValue $Template.GUID -Force + if ($CurrentStandard.action.value -contains 'Remediate' -and -not ($CurrentStandard.action.value -contains 'Report')) { + $reportAction = [pscustomobject]@{ + label = 'Report' + value = 'Report' + } + $CurrentStandard.action = @($CurrentStandard.action) + $reportAction + } + $Actions = $CurrentStandard.action.value if ($Actions -contains 'Remediate' -or $Actions -contains 'warn' -or $Actions -contains 'Report') { if (-not $ComputedStandards.Contains($StandardName)) { @@ -230,6 +262,14 @@ function Get-CIPPStandards { $CurrentStandard = $Item.PSObject.Copy() $CurrentStandard | Add-Member -NotePropertyName 'TemplateId' -NotePropertyValue $Template.GUID -Force + if ($CurrentStandard.action.value -contains 'Remediate' -and -not ($CurrentStandard.action.value -contains 'Report')) { + $reportAction = [pscustomobject]@{ + label = 'Report' + value = 'Report' + } + $CurrentStandard.action = @($CurrentStandard.action) + $reportAction + } + # Filter actions only 'Remediate','warn','Report' $Actions = $CurrentStandard.action.value | Where-Object { $_ -in 'Remediate', 'warn', 'Report' } if ($Actions -contains 'Remediate' -or $Actions -contains 'warn' -or $Actions -contains 'Report') { @@ -245,6 +285,14 @@ function Get-CIPPStandards { $CurrentStandard = $Value.PSObject.Copy() $CurrentStandard | Add-Member -NotePropertyName 'TemplateId' -NotePropertyValue $Template.GUID -Force + if ($CurrentStandard.action.value -contains 'Remediate' -and -not ($CurrentStandard.action.value -contains 'Report')) { + $reportAction = [pscustomobject]@{ + label = 'Report' + value = 'Report' + } + $CurrentStandard.action = @($CurrentStandard.action) + $reportAction + } + $Actions = $CurrentStandard.action.value | Where-Object { $_ -in 'Remediate', 'warn', 'Report' } if ($Actions -contains 'Remediate' -or $Actions -contains 'warn' -or $Actions -contains 'Report') { if (-not $ComputedStandards.Contains($StandardName)) { From 7613d60e37ab92c9b0093f9fd47271938f277da8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Thu, 19 Jun 2025 18:27:16 +0200 Subject: [PATCH 133/160] Refactor Invoke-ExecSetOoO function to improve error handling and logging. Updated header variable usage and streamlined result assignment for out-of-office settings. --- .../Administration/Invoke-ExecSetOoO.ps1 | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecSetOoO.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecSetOoO.ps1 index c3c814a7712c..9fc522341472 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecSetOoO.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecSetOoO.ps1 @@ -11,9 +11,13 @@ Function Invoke-ExecSetOoO { param($Request, $TriggerMetadata) try { $APIName = $Request.Params.CIPPEndpoint - Write-LogMessage -headers $Request.Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + + $Username = $Request.Body.userId $TenantFilter = $Request.Body.tenantFilter + if ($Request.Body.input) { $InternalMessage = $Request.Body.input $ExternalMessage = $Request.Body.input @@ -25,25 +29,25 @@ Function Invoke-ExecSetOoO { $StartTime = if ($Request.Body.StartTime -match '^\d+$') { [DateTimeOffset]::FromUnixTimeSeconds([int]$Request.Body.StartTime).DateTime } else { $Request.Body.StartTime } $EndTime = if ($Request.Body.EndTime -match '^\d+$') { [DateTimeOffset]::FromUnixTimeSeconds([int]$Request.Body.EndTime).DateTime } else { $Request.Body.EndTime } - $Results = try { - if ($Request.Body.AutoReplyState.value -ne 'Scheduled') { - Set-CIPPOutOfOffice -userid $Username -tenantFilter $TenantFilter -APIName $APIName -Headers $Request.Headers -InternalMessage $InternalMessage -ExternalMessage $ExternalMessage -State $Request.Body.AutoReplyState.value - } else { - Set-CIPPOutOfOffice -userid $Username -tenantFilter $TenantFilter -APIName $APIName -Headers $Request.Headers -InternalMessage $InternalMessage -ExternalMessage $ExternalMessage -StartTime $StartTime -EndTime $EndTime -State $Request.Body.AutoReplyState.value - } - } catch { - "Could not add out of office message for $($Username). Error: $($_.Exception.Message)" + + if ($Request.Body.AutoReplyState.value -ne 'Scheduled') { + $Results = Set-CIPPOutOfOffice -userid $Username -tenantFilter $TenantFilter -APIName $APIName -Headers $Headers -InternalMessage $InternalMessage -ExternalMessage $ExternalMessage -State $Request.Body.AutoReplyState.value + } else { + $Results = Set-CIPPOutOfOffice -userid $Username -tenantFilter $TenantFilter -APIName $APIName -Headers $Headers -InternalMessage $InternalMessage -ExternalMessage $ExternalMessage -StartTime $StartTime -EndTime $EndTime -State $Request.Body.AutoReplyState.value } - $Body = [PSCustomObject]@{'Results' = $($Results) } + } catch { - $Body = [PSCustomObject]@{'Results' = "Could not set Out of Office user: $($_.Exception.Message)" } + $ErrorMessage = Get-CippException -Exception $_ + $Results = "Could not set Out of Office for user: $($Username). Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -message $Results -Sev 'Error' -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::InternalServerError } # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ - StatusCode = [HttpStatusCode]::OK - Body = $Body + StatusCode = $StatusCode + Body = @{'Results' = $($Results) } }) } From 3b48379d65315d136e45bfa2f09bd28006c4432b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Thu, 19 Jun 2025 19:10:45 +0200 Subject: [PATCH 134/160] Enhance Set-CIPPOutOfOffice function by enforcing mandatory parameters and validating state options. Refactor parameter handling for improved clarity and error messaging. Update Invoke-ExecSetOoO to utilize splatting for cleaner code. --- .../Administration/Invoke-ExecSetOoO.ps1 | 25 ++++++++--- .../Users/Invoke-CIPPOffboardingJob.ps1 | 6 ++- .../CIPPCore/Public/Set-CIPPOutOfoffice.ps1 | 45 ++++++++++++++----- 3 files changed, 58 insertions(+), 18 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecSetOoO.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecSetOoO.ps1 index 9fc522341472..115f2f404d53 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecSetOoO.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecSetOoO.ps1 @@ -17,6 +17,7 @@ Function Invoke-ExecSetOoO { $Username = $Request.Body.userId $TenantFilter = $Request.Body.tenantFilter + $State = $Request.Body.AutoReplyState.value if ($Request.Body.input) { $InternalMessage = $Request.Body.input @@ -26,16 +27,28 @@ Function Invoke-ExecSetOoO { $ExternalMessage = $Request.Body.ExternalMessage } #if starttime and endtime are a number, they are unix timestamps and need to be converted to datetime, otherwise just use them. - $StartTime = if ($Request.Body.StartTime -match '^\d+$') { [DateTimeOffset]::FromUnixTimeSeconds([int]$Request.Body.StartTime).DateTime } else { $Request.Body.StartTime } - $EndTime = if ($Request.Body.EndTime -match '^\d+$') { [DateTimeOffset]::FromUnixTimeSeconds([int]$Request.Body.EndTime).DateTime } else { $Request.Body.EndTime } + $StartTime = $Request.Body.StartTime -match '^\d+$' ? [DateTimeOffset]::FromUnixTimeSeconds([int]$Request.Body.StartTime).DateTime : $Request.Body.StartTime + $EndTime = $Request.Body.EndTime -match '^\d+$' ? [DateTimeOffset]::FromUnixTimeSeconds([int]$Request.Body.EndTime).DateTime : $Request.Body.EndTime - if ($Request.Body.AutoReplyState.value -ne 'Scheduled') { - $Results = Set-CIPPOutOfOffice -userid $Username -tenantFilter $TenantFilter -APIName $APIName -Headers $Headers -InternalMessage $InternalMessage -ExternalMessage $ExternalMessage -State $Request.Body.AutoReplyState.value - } else { - $Results = Set-CIPPOutOfOffice -userid $Username -tenantFilter $TenantFilter -APIName $APIName -Headers $Headers -InternalMessage $InternalMessage -ExternalMessage $ExternalMessage -StartTime $StartTime -EndTime $EndTime -State $Request.Body.AutoReplyState.value + $SplatParams = @{ + userid = $Username + tenantFilter = $TenantFilter + APIName = $APIName + Headers = $Headers + InternalMessage = $InternalMessage + ExternalMessage = $ExternalMessage + State = $State + } + + # If the state is scheduled, add the start and end times to the splat params + if ($State -eq 'Scheduled') { + $SplatParams.StartTime = $StartTime + $SplatParams.EndTime = $EndTime } + $Results = Set-CIPPOutOfOffice @SplatParams + } catch { $ErrorMessage = Get-CippException -Exception $_ diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-CIPPOffboardingJob.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-CIPPOffboardingJob.ps1 index 7534156ee3ad..d0ad323f12f7 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-CIPPOffboardingJob.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-CIPPOffboardingJob.ps1 @@ -42,7 +42,11 @@ function Invoke-CIPPOffboardingJob { $Options.AccessAutomap | ForEach-Object { Set-CIPPMailboxAccess -tenantFilter $TenantFilter -userid $username -AccessUser $_.value -Automap $true -AccessRights @('FullAccess') -Headers $Headers -APIName $APIName } } { $_.OOO } { - Set-CIPPOutOfOffice -tenantFilter $TenantFilter -userid $username -InternalMessage $Options.OOO -ExternalMessage $Options.OOO -Headers $Headers -APIName $APIName -state 'Enabled' + try { + Set-CIPPOutOfOffice -tenantFilter $TenantFilter -UserID $username -InternalMessage $Options.OOO -ExternalMessage $Options.OOO -Headers $Headers -APIName $APIName -state 'Enabled' + } catch { + $_.Exception.Message + } } { $_.forward } { if (!$Options.KeepCopy) { diff --git a/Modules/CIPPCore/Public/Set-CIPPOutOfoffice.ps1 b/Modules/CIPPCore/Public/Set-CIPPOutOfoffice.ps1 index 6ee8d1ea49b1..811a241321fe 100644 --- a/Modules/CIPPCore/Public/Set-CIPPOutOfoffice.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPOutOfoffice.ps1 @@ -1,11 +1,14 @@ function Set-CIPPOutOfOffice { [CmdletBinding()] param ( - $userid, + [Parameter(Mandatory = $true)] + $UserID, $InternalMessage, $ExternalMessage, $TenantFilter, - $State, + [ValidateSet('Enabled', 'Disabled', 'Scheduled')] + [Parameter(Mandatory = $true)] + [string]$State, $APIName = 'Set Out of Office', $Headers, $StartTime, @@ -19,18 +22,38 @@ function Set-CIPPOutOfOffice { if (-not $EndTime) { $EndTime = (Get-Date $StartTime).AddDays(7) } - if ($State -ne 'Scheduled') { - $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Set-MailboxAutoReplyConfiguration' -cmdParams @{Identity = $userid; AutoReplyState = $State; InternalMessage = $InternalMessage; ExternalMessage = $ExternalMessage } -Anchor $userid - Write-LogMessage -headers $Headers -API $APIName -message "Set Out-of-office for $($userid) to $State" -Sev 'Info' -tenant $TenantFilter - return "Set Out-of-office for $($userid) to $State." + $CmdParams = @{ + Identity = $UserID + AutoReplyState = $State + } + + if (-not [string]::IsNullOrWhiteSpace($InternalMessage)) { + $CmdParams.InternalMessage = $InternalMessage + } + + if (-not [string]::IsNullOrWhiteSpace($ExternalMessage)) { + $CmdParams.ExternalMessage = $ExternalMessage + } + + if ($State -eq 'Scheduled') { + $CmdParams.StartTime = $StartTime + $CmdParams.EndTime = $EndTime + } + + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Set-MailboxAutoReplyConfiguration' -cmdParams $CmdParams -Anchor $UserID + + if ($State -eq 'Scheduled') { + $Results = "Scheduled Out-of-office for $($UserID) between $($StartTime.toString()) and $($EndTime.toString())" + Write-LogMessage -headers $Headers -API $APIName -message $Results -Sev 'Info' -tenant $TenantFilter } else { - $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Set-MailboxAutoReplyConfiguration' -cmdParams @{Identity = $userid; AutoReplyState = $State; InternalMessage = $InternalMessage; ExternalMessage = $ExternalMessage; StartTime = $StartTime; EndTime = $EndTime } -Anchor $userid - Write-LogMessage -headers $Headers -API $APIName -message "Scheduled Out-of-office for $($userid) between $StartTime and $EndTime" -Sev 'Info' -tenant $TenantFilter - return "Scheduled Out-of-office for $($userid) between $($StartTime.toString()) and $($EndTime.toString())" + $Results = "Set Out-of-office for $($UserID) to $State." + Write-LogMessage -headers $Headers -API $APIName -message $Results -Sev 'Info' -tenant $TenantFilter } + return $Results } catch { $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -headers $Headers -API $APIName -message "Could not add OOO for $($userid). Error: $($ErrorMessage.NormalizedError)" -Sev 'Error' -tenant $TenantFilter -LogData $ErrorMessage - return "Could not add out of office message for $($userid). Error: $($ErrorMessage.NormalizedError)" + $Results = "Could not add OOO for $($UserID). Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -message $Results -Sev 'Error' -tenant $TenantFilter -LogData $ErrorMessage + throw $Results } } From 8f58eae943e09de491a5f5b4a6343de083dd7e67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Thu, 19 Jun 2025 19:21:02 +0200 Subject: [PATCH 135/160] fix idiotic bug --- .../Email-Exchange/Administration/Invoke-ExecSetOoO.ps1 | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecSetOoO.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecSetOoO.ps1 index 115f2f404d53..5bdfd1c186f5 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecSetOoO.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecSetOoO.ps1 @@ -48,8 +48,7 @@ Function Invoke-ExecSetOoO { } $Results = Set-CIPPOutOfOffice @SplatParams - - + $StatusCode = [HttpStatusCode]::OK } catch { $ErrorMessage = Get-CippException -Exception $_ $Results = "Could not set Out of Office for user: $($Username). Error: $($ErrorMessage.NormalizedError)" From ee0683216c99bc8c03e3e242d34b55d6065aaa6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Thu, 19 Jun 2025 19:55:49 +0200 Subject: [PATCH 136/160] Change to return status code based on result of request Refactor Get-CIPPOutOfOffice to throw results on failure --- .../Administration/Invoke-ListOoO.ps1 | 18 +++++++++++------- .../CIPPCore/Public/Get-CIPPOutOfOffice.ps1 | 10 ++++++---- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListOoO.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListOoO.ps1 index 2a1842db4d5b..3bc1e1b42701 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListOoO.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListOoO.ps1 @@ -11,19 +11,23 @@ Function Invoke-ListOoO { param($Request, $TriggerMetadata) $APIName = $Request.Params.CIPPEndpoint - $Tenantfilter = $request.query.tenantFilter + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + + $TenantFilter = $Request.Query.tenantFilter + $UserID = $Request.Query.userid try { - $Body = Get-CIPPOutOfOffice -UserID $Request.query.userid -tenantFilter $TenantFilter -APIName $APINAME -Headers $Request.Headers + $Results = Get-CIPPOutOfOffice -UserID $UserID -tenantFilter $TenantFilter -APIName $APIName -Headers $Headers + $StatusCode = [HttpStatusCode]::OK } catch { - $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - $Body = [pscustomobject]@{'Results' = "Failed. $ErrorMessage" } - + $Results = $_.Exception.Message + $StatusCode = [HttpStatusCode]::InternalServerError } # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ - StatusCode = [HttpStatusCode]::OK - Body = $Body + StatusCode = $StatusCode + Body = $Results }) } diff --git a/Modules/CIPPCore/Public/Get-CIPPOutOfOffice.ps1 b/Modules/CIPPCore/Public/Get-CIPPOutOfOffice.ps1 index 2f5e45739c69..c5b0af944f3c 100644 --- a/Modules/CIPPCore/Public/Get-CIPPOutOfOffice.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPOutOfOffice.ps1 @@ -1,14 +1,14 @@ function Get-CIPPOutOfOffice { [CmdletBinding()] param ( - $userid, + $UserID, $TenantFilter, $APIName = 'Get Out of Office', $Headers ) try { - $OutOfOffice = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-MailboxAutoReplyConfiguration' -cmdParams @{Identity = $userid } -Anchor $userid + $OutOfOffice = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-MailboxAutoReplyConfiguration' -cmdParams @{Identity = $UserID } -Anchor $UserID $Results = @{ AutoReplyState = $OutOfOffice.AutoReplyState StartTime = $OutOfOffice.StartTime.ToString('yyyy-MM-dd HH:mm') @@ -18,7 +18,9 @@ function Get-CIPPOutOfOffice { } | ConvertTo-Json return $Results } catch { - $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - return "Could not retrieve out of office message for $($userid). Error: $ErrorMessage" + $ErrorMessage = Get-CippException -Exception $_ + $Results = "Could not retrieve out of office message for $($UserID). Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -message $Results -Sev 'Error' -LogData $ErrorMessage + throw $Results } } From f3b0051f6ad6de8de3f4a3ff9605b1422aa5c32b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Thu, 19 Jun 2025 20:05:18 +0200 Subject: [PATCH 137/160] Refactor Set-CIPPOutOfOffice and Invoke-ExecSetOoO functions to improve parameter handling and logging. Simplified message assignment and ensured default values for StartTime and EndTime are set correctly when not provided. Enhanced clarity in handling internal and external messages. --- .../Administration/Invoke-ExecSetOoO.ps1 | 35 ++++++++++++------- .../CIPPCore/Public/Set-CIPPOutOfoffice.ps1 | 26 ++++++-------- 2 files changed, 33 insertions(+), 28 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecSetOoO.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecSetOoO.ps1 index 5bdfd1c186f5..1bd62dac8eb9 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecSetOoO.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecSetOoO.ps1 @@ -19,30 +19,39 @@ Function Invoke-ExecSetOoO { $TenantFilter = $Request.Body.tenantFilter $State = $Request.Body.AutoReplyState.value + $SplatParams = @{ + userid = $Username + tenantFilter = $TenantFilter + APIName = $APIName + Headers = $Headers + State = $State + } + + # User action uses input, edit exchange uses InternalMessage and ExternalMessage + # User action disable OoO doesn't send any input if ($Request.Body.input) { $InternalMessage = $Request.Body.input $ExternalMessage = $Request.Body.input } else { $InternalMessage = $Request.Body.InternalMessage $ExternalMessage = $Request.Body.ExternalMessage - } - #if starttime and endtime are a number, they are unix timestamps and need to be converted to datetime, otherwise just use them. - $StartTime = $Request.Body.StartTime -match '^\d+$' ? [DateTimeOffset]::FromUnixTimeSeconds([int]$Request.Body.StartTime).DateTime : $Request.Body.StartTime - $EndTime = $Request.Body.EndTime -match '^\d+$' ? [DateTimeOffset]::FromUnixTimeSeconds([int]$Request.Body.EndTime).DateTime : $Request.Body.EndTime - - $SplatParams = @{ - userid = $Username - tenantFilter = $TenantFilter - APIName = $APIName - Headers = $Headers - InternalMessage = $InternalMessage - ExternalMessage = $ExternalMessage - State = $State + # Only add the internal and external message if they are not empty/null. Done to be able to set the OOO to disabled, while keeping the existing messages intact. + # This works because the frontend always sends some HTML even if the fields are empty. + if (-not [string]::IsNullOrWhiteSpace($InternalMessage)) { + $SplatParams.InternalMessage = $InternalMessage + } + if (-not [string]::IsNullOrWhiteSpace($ExternalMessage)) { + $SplatParams.ExternalMessage = $ExternalMessage + } } + # If the state is scheduled, add the start and end times to the splat params if ($State -eq 'Scheduled') { + # If starttime and endtime are a number, they are unix timestamps and need to be converted to datetime, otherwise just use them. + $StartTime = $Request.Body.StartTime -match '^\d+$' ? [DateTimeOffset]::FromUnixTimeSeconds([int]$Request.Body.StartTime).DateTime : $Request.Body.StartTime + $EndTime = $Request.Body.EndTime -match '^\d+$' ? [DateTimeOffset]::FromUnixTimeSeconds([int]$Request.Body.EndTime).DateTime : $Request.Body.EndTime $SplatParams.StartTime = $StartTime $SplatParams.EndTime = $EndTime } diff --git a/Modules/CIPPCore/Public/Set-CIPPOutOfoffice.ps1 b/Modules/CIPPCore/Public/Set-CIPPOutOfoffice.ps1 index 811a241321fe..ced3fad0a5af 100644 --- a/Modules/CIPPCore/Public/Set-CIPPOutOfoffice.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPOutOfoffice.ps1 @@ -16,39 +16,35 @@ function Set-CIPPOutOfOffice { ) try { - if (-not $StartTime) { - $StartTime = (Get-Date).ToString() - } - if (-not $EndTime) { - $EndTime = (Get-Date $StartTime).AddDays(7) - } + $CmdParams = @{ Identity = $UserID AutoReplyState = $State } - if (-not [string]::IsNullOrWhiteSpace($InternalMessage)) { + if ($PSBoundParameters.ContainsKey('InternalMessage')) { $CmdParams.InternalMessage = $InternalMessage } - if (-not [string]::IsNullOrWhiteSpace($ExternalMessage)) { + if ($PSBoundParameters.ContainsKey('ExternalMessage')) { $CmdParams.ExternalMessage = $ExternalMessage } if ($State -eq 'Scheduled') { + # If starttime or endtime are not provided, default to enabling OOO for 7 days + $StartTime = $StartTime ? $StartTime : (Get-Date).ToString() + $EndTime = $EndTime ? $EndTime : (Get-Date $StartTime).AddDays(7) $CmdParams.StartTime = $StartTime $CmdParams.EndTime = $EndTime } $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Set-MailboxAutoReplyConfiguration' -cmdParams $CmdParams -Anchor $UserID - if ($State -eq 'Scheduled') { - $Results = "Scheduled Out-of-office for $($UserID) between $($StartTime.toString()) and $($EndTime.toString())" - Write-LogMessage -headers $Headers -API $APIName -message $Results -Sev 'Info' -tenant $TenantFilter - } else { - $Results = "Set Out-of-office for $($UserID) to $State." - Write-LogMessage -headers $Headers -API $APIName -message $Results -Sev 'Info' -tenant $TenantFilter - } + $Results = $State -eq 'Scheduled' ? + "Scheduled Out-of-office for $($UserID) between $($StartTime.toString()) and $($EndTime.toString())" : + "Set Out-of-office for $($UserID) to $State." + + Write-LogMessage -headers $Headers -API $APIName -message $Results -Sev 'Info' -tenant $TenantFilter return $Results } catch { $ErrorMessage = Get-CippException -Exception $_ From eeac02200c85a0ea60ece64399042205cd8fbe30 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 10 Jun 2025 18:25:39 +0200 Subject: [PATCH 138/160] Re-add Set-CIPPCalendarPermission refactor code that was erroneously removed. Remove some redundant logging Fix casing --- .../Invoke-ExecModifyCalPerms.ps1 | 121 +++++++----------- 1 file changed, 48 insertions(+), 73 deletions(-) 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 2431861d5c3e..957b0acb0dad 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 @@ -11,16 +11,17 @@ Function Invoke-ExecModifyCalPerms { param($Request, $TriggerMetadata) $APIName = $Request.Params.CIPPEndpoint - Write-LogMessage -headers $Request.Headers -API $APINAME-message 'Accessed this API' -Sev 'Debug' - - $Username = $request.body.userID - $Tenantfilter = $request.body.tenantfilter - $Permissions = $request.body.permissions + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' - Write-LogMessage -headers $Request.Headers -API $APINAME-message "Processing request for user: $Username, tenant: $Tenantfilter" -Sev 'Debug' + $Username = $Request.Body.userID + $TenantFilter = $Request.Body.tenantFilter + $Permissions = $Request.Body.permissions - if ($username -eq $null) { - Write-LogMessage -headers $Request.Headers -API $APINAME-message 'Username is null' -Sev 'Error' + Write-LogMessage -headers $Headers -API $APIName -message "Processing request for user: $Username, tenant: $TenantFilter" -Sev 'Debug' + + if ($null -eq $Username) { + Write-LogMessage -headers $Headers -API $APIName -message 'Username is null' -Sev 'Error' $body = [pscustomobject]@{'Results' = @('Username is required') } Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::BadRequest @@ -28,13 +29,12 @@ Function Invoke-ExecModifyCalPerms { }) return } - + try { - $userid = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$($username)" -tenantid $Tenantfilter).id - Write-LogMessage -headers $Request.Headers -API $APINAME-message "Retrieved user ID: $userid" -Sev 'Debug' - } - catch { - Write-LogMessage -headers $Request.Headers -API $APINAME-message "Failed to get user ID: $($_.Exception.Message)" -Sev 'Error' + $UserId = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$($Username)" -tenantid $TenantFilter).id + Write-LogMessage -headers $Headers -API $APIName -message "Retrieved user ID: $UserId" -Sev 'Debug' + } catch { + Write-LogMessage -headers $Headers -API $APIName -message "Failed to get user ID: $($_.Exception.Message)" -Sev 'Error' $body = [pscustomobject]@{'Results' = @("Failed to get user ID: $($_.Exception.Message)") } Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::NotFound @@ -43,98 +43,73 @@ Function Invoke-ExecModifyCalPerms { return } - $Results = [System.Collections.ArrayList]::new() + $Results = [System.Collections.Generic.List[string]]::new() $HasErrors = $false # Convert permissions to array format if it's an object with numeric keys if ($Permissions -is [PSCustomObject]) { if ($Permissions.PSObject.Properties.Name -match '^\d+$') { $Permissions = $Permissions.PSObject.Properties.Value - } - else { + } else { $Permissions = @($Permissions) } } - Write-LogMessage -headers $Request.Headers -API $APINAME-message "Processing $($Permissions.Count) permission entries" -Sev 'Debug' + Write-LogMessage -headers $Headers -API $APIName -message "Processing $($Permissions.Count) permission entries" -Sev 'Debug' foreach ($Permission in $Permissions) { - Write-LogMessage -headers $Request.Headers -API $APINAME-message "Processing permission: $($Permission | ConvertTo-Json)" -Sev 'Debug' - + Write-LogMessage -headers $Headers -API $APIName -message "Processing permission: $($Permission | ConvertTo-Json)" -Sev 'Debug' + $PermissionLevel = $Permission.PermissionLevel.value ?? $Permission.PermissionLevel $Modification = $Permission.Modification $CanViewPrivateItems = $Permission.CanViewPrivateItems ?? $false - - Write-LogMessage -headers $Request.Headers -API $APINAME-message "Permission Level: $PermissionLevel, Modification: $Modification, CanViewPrivateItems: $CanViewPrivateItems" -Sev 'Debug' - + $FolderName = $Permission.FolderName ?? 'Calendar' + + Write-LogMessage -headers $Headers -API $APIName -message "Permission Level: $PermissionLevel, Modification: $Modification, CanViewPrivateItems: $CanViewPrivateItems, FolderName: $FolderName" -Sev 'Debug' + # Handle UserID as array or single value $TargetUsers = @($Permission.UserID | ForEach-Object { $_.value ?? $_ }) - Write-LogMessage -headers $Request.Headers -API $APINAME-message "Target Users: $($TargetUsers -join ', ')" -Sev 'Debug' + Write-LogMessage -headers $Headers -API $APIName -message "Target Users: $($TargetUsers -join ', ')" -Sev 'Debug' foreach ($TargetUser in $TargetUsers) { try { - Write-LogMessage -headers $Request.Headers -API $APINAME-message "Processing target user: $TargetUser" -Sev 'Debug' - - if ($Modification -eq 'Remove') { - try { - $CalPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Remove-MailboxFolderPermission' -cmdParams @{ - Identity = "$($userid):\Calendar" - User = $TargetUser - Confirm = $false - } - $null = $results.Add("Removed $($TargetUser) from $($username) Calendar permissions") - } - catch { - $null = $results.Add("No existing permissions to remove for $($TargetUser)") - } - } - else { - Write-LogMessage -headers $Request.Headers -API $APINAME-message "Setting permissions with AccessRights: $PermissionLevel" -Sev 'Debug' - - $cmdParams = @{ - Identity = "$($userid):\Calendar" - User = $TargetUser - AccessRights = $PermissionLevel - Confirm = $false - } - - if ($CanViewPrivateItems) { - $cmdParams['SharingPermissionFlags'] = 'Delegate,CanViewPrivateItems' - } - - try { - # Try Add first - $CalPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Add-MailboxFolderPermission' -cmdParams $cmdParams - $null = $results.Add("Granted $($TargetUser) $($PermissionLevel) access to $($username) Calendar$($CanViewPrivateItems ? ' with access to private items' : '')") - } - catch { - # If Add fails, try Set - $CalPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Set-MailboxFolderPermission' -cmdParams $cmdParams - $null = $results.Add("Updated $($TargetUser) $($PermissionLevel) access to $($username) Calendar$($CanViewPrivateItems ? ' with access to private items' : '')") - } + 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 } - Write-LogMessage -headers $Request.Headers -API $APINAME-message "Successfully executed $($PermissionLevel) permission modification for $($TargetUser) on $($username)" -Sev 'Info' -tenant $TenantFilter - } - catch { + + # Write-Host "Request params: $($Params | ConvertTo-Json)" + $Result = Set-CIPPCalendarPermission @Params + + $null = $Results.Add($Result) + } catch { $HasErrors = $true - Write-LogMessage -headers $Request.Headers -API $APINAME-message "Could not execute $($PermissionLevel) permission modification for $($TargetUser) on $($username). Error: $($_.Exception.Message)" -Sev 'Error' -tenant $TenantFilter - $null = $results.Add("Could not execute $($PermissionLevel) permission modification for $($TargetUser) on $($username). Error: $($_.Exception.Message)") + $null = $Results.Add("$($_.Exception.Message)") } } } - if ($results.Count -eq 0) { - Write-LogMessage -headers $Request.Headers -API $APINAME-message 'No results were generated from the operation' -Sev 'Warning' - $null = $results.Add('No results were generated from the operation. Please check the logs for more details.') + if ($Results.Count -eq 0) { + Write-LogMessage -headers $Headers -API $APIName -message 'No results were generated from the operation' -Sev 'Warning' + $null = $Results.Add('No results were generated from the operation. Please check the logs for more details.') $HasErrors = $true } - $body = [pscustomobject]@{'Results' = @($results) } + $Body = [pscustomobject]@{'Results' = @($Results) } # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ StatusCode = if ($HasErrors) { [HttpStatusCode]::InternalServerError } else { [HttpStatusCode]::OK } Body = $Body }) -} \ No newline at end of file +} From 8afde120805fc995601ac80a8f918d8868e6a4d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Thu, 19 Jun 2025 21:54:12 +0200 Subject: [PATCH 139/160] Refactor Invoke-ListDefenderTVM to return all properties and future ones. WORD --- .../Endpoint/MEM/Invoke-ListDefenderTVM.ps1 | 36 ++++++++++++------- cspell.json | 5 +-- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListDefenderTVM.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListDefenderTVM.ps1 index d699ab7fbb10..fde70dcc558a 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListDefenderTVM.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListDefenderTVM.ps1 @@ -18,21 +18,31 @@ Function Invoke-ListDefenderTVM { # Interact with query parameters or the body of the request. try { - $GraphRequest = New-GraphgetRequest -tenantid $TenantFilter -uri "https://api.securitycenter.microsoft.com/api/machines/SoftwareVulnerabilitiesByMachine?`$top=999" -scope 'https://api.securitycenter.microsoft.com/.default' | Group-Object cveid + $GraphRequest = New-GraphGetRequest -tenantid $TenantFilter -uri "https://api.securitycenter.microsoft.com/api/machines/SoftwareVulnerabilitiesByMachine?`$top=999" -scope 'https://api.securitycenter.microsoft.com/.default' | Group-Object cveId $GroupObj = foreach ($cve in $GraphRequest) { - [pscustomobject]@{ - customerId = $TenantFilter - affectedDevicesCount = $cve.count - cveId = $cve.name - affectedDevices = ($cve.group.deviceName -join ', ') - osPlatform = ($cve.group.osplatform | Sort-Object -Unique) - softwareVendor = ($cve.group.softwareVendor | Sort-Object -Unique) - softwareName = ($cve.group.softwareName | Sort-Object -Unique) - vulnerabilitySeverityLevel = ($cve.group.vulnerabilitySeverityLevel | Sort-Object -Unique) - cvssScore = ($cve.group.cvssScore | Sort-Object -Unique) - securityUpdateAvailable = ($cve.group.securityUpdateAvailable | Sort-Object -Unique) - exploitabilityLevel = ($cve.group.exploitabilityLevel | Sort-Object -Unique) + # Start with base properties + $obj = [ordered]@{ + customerId = $TenantFilter + affectedDevicesCount = $cve.count + cveId = $cve.name } + + # Get all unique property names from the group + $allProperties = $cve.group | Get-Member -MemberType Properties | Select-Object -ExpandProperty Name | Sort-Object -Unique + + # Add all properties from the group with appropriate processing + foreach ($property in $allProperties) { + if ($property -eq 'deviceName') { + # Special handling for deviceName - join with comma + $obj['affectedDevices'] = ($cve.group.$property -join ', ') + } else { + # For all other properties, get unique values + $obj[$property] = ($cve.group.$property | Sort-Object -Unique) | Select-Object -First 1 + } + } + + # Convert and output as PSCustomObject. Not really needed, but hey, why not. + [pscustomobject]$obj } $StatusCode = [HttpStatusCode]::OK } catch { diff --git a/cspell.json b/cspell.json index 3d3fd4853c5c..e4ec9aadb5ed 100644 --- a/cspell.json +++ b/cspell.json @@ -14,9 +14,11 @@ "Connectwise", "CPIM", "Datto", + "DMARC", "endswith", "entra", "Entra", + "exploitability", "gdap", "GDAP", "IMAP", @@ -39,8 +41,7 @@ "TNEF", "weburl", "winmail", - "Yubikey", - "DMARC" + "Yubikey" ], "ignoreWords": [ "ACOM", From 0a375bd41547e5922bab9569949ddf405bc68fe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Thu, 19 Jun 2025 23:04:14 +0200 Subject: [PATCH 140/160] Feat: add Get-CIPPAlertVulnerabilities alert --- .../Alerts/Get-CIPPAlertVulnerabilities.ps1 | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 Modules/CIPPCore/Public/Alerts/Get-CIPPAlertVulnerabilities.ps1 diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertVulnerabilities.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertVulnerabilities.ps1 new file mode 100644 index 000000000000..3ace81f1b886 --- /dev/null +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertVulnerabilities.ps1 @@ -0,0 +1,64 @@ +function Get-CIPPAlertVulnerabilities { + <# + .FUNCTIONALITY + Entrypoint + #> + [CmdletBinding()] + Param ( + [Parameter(Mandatory = $false)] + [Alias('input')] + $InputValue, + $TenantFilter + ) + + try { + $VulnerabilityRequest = New-GraphGetRequest -tenantid $TenantFilter -uri "https://api.securitycenter.microsoft.com/api/machines/SoftwareVulnerabilitiesByMachine?`$top=999&`$filter=cveId ne null" -scope 'https://api.securitycenter.microsoft.com/.default' + + if ($VulnerabilityRequest) { + $AlertData = [System.Collections.Generic.List[PSCustomObject]]::new() + + # Group by CVE ID and create objects for each vulnerability + $VulnerabilityGroups = $VulnerabilityRequest | Where-Object { $_.cveId } | Group-Object cveId + + foreach ($Group in $VulnerabilityGroups) { + $FirstVuln = $Group.Group | Sort-Object firstSeenTimestamp | Select-Object -First 1 + $AffectedDevices = ($Group.Group | Select-Object -ExpandProperty deviceName -Unique) -join ', ' + $DaysOld = [math]::Round(((Get-Date) - [datetime]$FirstVuln.firstSeenTimestamp).TotalDays) + $HoursOld = [math]::Round(((Get-Date) - [datetime]$FirstVuln.firstSeenTimestamp).TotalHours) + + # Skip if vulnerability is not old enough + if ($HoursOld -lt [int]$InputValue) { + continue + } + + $VulnerabilityAlert = [PSCustomObject]@{ + CVE = $Group.Name + Severity = $FirstVuln.vulnerabilitySeverityLevel + FirstSeenTimestamp = $FirstVuln.firstSeenTimestamp + LastSeenTimestamp = $FirstVuln.lastSeenTimestamp + DaysOld = $DaysOld + HoursOld = $HoursOld + AffectedDeviceCount = $Group.Count + AffectedDevices = $AffectedDevices + SoftwareName = $FirstVuln.softwareName + SoftwareVendor = $FirstVuln.softwareVendor + SoftwareVersion = $FirstVuln.softwareVersion + CVSSScore = $FirstVuln.cvssScore + ExploitabilityLevel = $FirstVuln.exploitabilityLevel + RecommendedUpdate = $FirstVuln.recommendedSecurityUpdate + RecommendedUpdateId = $FirstVuln.recommendedSecurityUpdateId + RecommendedUpdateUrl = $FirstVuln.recommendedSecurityUpdateUrl + Tenant = $TenantFilter + } + $AlertData.Add($VulnerabilityAlert) + } + + # Only send alert if we have vulnerabilities that meet the criteria + if ($AlertData.Count -gt 0) { + Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData + } + } + } catch { + Write-LogMessage -message "Failed to check vulnerabilities: $($_.exception.message)" -API 'Vulnerability Alerts' -tenant $TenantFilter -sev Error + } +} From 248213dfa8de06ff561abf5cda6234b1879e32f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Thu, 19 Jun 2025 23:06:46 +0200 Subject: [PATCH 141/160] More efficient continue placement --- .../CIPPCore/Public/Alerts/Get-CIPPAlertVulnerabilities.ps1 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertVulnerabilities.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertVulnerabilities.ps1 index 3ace81f1b886..ad1ff546b89c 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertVulnerabilities.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertVulnerabilities.ps1 @@ -22,8 +22,6 @@ function Get-CIPPAlertVulnerabilities { foreach ($Group in $VulnerabilityGroups) { $FirstVuln = $Group.Group | Sort-Object firstSeenTimestamp | Select-Object -First 1 - $AffectedDevices = ($Group.Group | Select-Object -ExpandProperty deviceName -Unique) -join ', ' - $DaysOld = [math]::Round(((Get-Date) - [datetime]$FirstVuln.firstSeenTimestamp).TotalDays) $HoursOld = [math]::Round(((Get-Date) - [datetime]$FirstVuln.firstSeenTimestamp).TotalHours) # Skip if vulnerability is not old enough @@ -31,6 +29,9 @@ function Get-CIPPAlertVulnerabilities { continue } + $DaysOld = [math]::Round(((Get-Date) - [datetime]$FirstVuln.firstSeenTimestamp).TotalDays) + $AffectedDevices = ($Group.Group | Select-Object -ExpandProperty deviceName -Unique) -join ', ' + $VulnerabilityAlert = [PSCustomObject]@{ CVE = $Group.Name Severity = $FirstVuln.vulnerabilitySeverityLevel From 81b2ffc656b766e774e49a623cdf1799fe63918c Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 20 Jun 2025 00:02:11 +0200 Subject: [PATCH 142/160] branding api --- .../Settings/Invoke-ExecBrandingSettings.ps1 | 125 ++++++++++++++++++ .../Users/Invoke-ListUserSettings.ps1 | 8 ++ 2 files changed, 133 insertions(+) create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecBrandingSettings.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecBrandingSettings.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecBrandingSettings.ps1 new file mode 100644 index 000000000000..d606b6d28da0 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecBrandingSettings.ps1 @@ -0,0 +1,125 @@ +using namespace System.Net + +Function Invoke-ExecBrandingSettings { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + CIPP.AppSettings.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + + $StatusCode = [HttpStatusCode]::OK + $Results = @{} + + try { + $Table = Get-CIPPTable -TableName Settings + $Filter = "PartitionKey eq 'BrandingSettings' and RowKey eq 'BrandingSettings'" + $BrandingConfig = Get-CIPPAzDataTableEntity @Table -Filter $Filter + + if (-not $BrandingConfig) { + $BrandingConfig = @{ + PartitionKey = 'BrandingSettings' + RowKey = 'BrandingSettings' + colour = '#F77F00' + logo = $null + } + } + + $Action = if ($Request.Body.Action) { $Request.Body.Action } else { $Request.Query.Action } + + switch ($Action) { + 'Get' { + $Results = @{ + colour = $BrandingConfig.colour + logo = $BrandingConfig.logo + } + } + 'Set' { + $Updated = $false + + if ($Request.Body.colour) { + $Colour = $Request.Body.colour + if ($Colour -match '^#[0-9A-Fa-f]{6}$') { + $BrandingConfig.colour = $Colour + $Updated = $true + } else { + $StatusCode = [HttpStatusCode]::BadRequest + $Results = 'Error: Invalid color format. Please use hex format (e.g., #F77F00)' + break + } + } + + if ($Request.Body.logo) { + $Logo = $Request.Body.logo + if ($Logo -match '^data:image\/') { + $Base64Data = $Logo -replace '^data:image\/[^;]+;base64,', '' + try { + $ImageBytes = [Convert]::FromBase64String($Base64Data) + if ($ImageBytes.Length -le 2097152) { + $BrandingConfig.logo = $Logo + $Updated = $true + } else { + $StatusCode = [HttpStatusCode]::BadRequest + $Results = 'Error: Image size must be less than 2MB' + break + } + } catch { + $StatusCode = [HttpStatusCode]::BadRequest + $Results = 'Error: Invalid base64 image data' + break + } + } elseif ($Logo -eq $null -or $Logo -eq '') { + $BrandingConfig.logo = $null + $Updated = $true + } + } + + if ($Updated) { + $BrandingConfig.PartitionKey = 'BrandingSettings' + $BrandingConfig.RowKey = 'BrandingSettings' + + Add-CIPPAzDataTableEntity @Table -Entity $BrandingConfig -Force | Out-Null + Write-LogMessage -API $APIName -tenant 'Global' -headers $Request.Headers -message 'Updated branding settings' -Sev 'Info' + $Results = 'Successfully updated branding settings' + } else { + $StatusCode = [HttpStatusCode]::BadRequest + $Results = 'Error: No valid branding data provided' + } + } + 'Reset' { + $DefaultConfig = @{ + PartitionKey = 'BrandingSettings' + RowKey = 'BrandingSettings' + colour = '#F77F00' + logo = $null + } + + Add-CIPPAzDataTableEntity @Table -Entity $DefaultConfig -Force | Out-Null + Write-LogMessage -API $APIName -tenant 'Global' -headers $Request.Headers -message 'Reset branding settings to defaults' -Sev 'Info' + $Results = 'Successfully reset branding settings to defaults' + } + default { + $StatusCode = [HttpStatusCode]::BadRequest + $Results = 'Error: Invalid action specified' + } + } + } catch { + Write-LogMessage -API $APIName -tenant 'Global' -headers $Request.Headers -message "Branding Settings API failed: $($_.Exception.Message)" -Sev 'Error' + $StatusCode = [HttpStatusCode]::InternalServerError + $Results = "Failed to process branding settings: $($_.Exception.Message)" + } + + $body = [pscustomobject]@{'Results' = $Results } + + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = $body + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserSettings.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserSettings.ps1 index 224584ab81af..901678b7b6ea 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserSettings.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserSettings.ps1 @@ -20,6 +20,14 @@ function Invoke-ListUserSettings { $UserSettings = Get-CIPPAzDataTableEntity @Table -Filter "RowKey eq 'allUsers'" if (!$UserSettings) { $UserSettings = Get-CIPPAzDataTableEntity @Table -Filter "RowKey eq '$Username'" } $UserSettings = $UserSettings.JSON | ConvertFrom-Json -Depth 10 -ErrorAction SilentlyContinue + #Get branding settings + if ($UserSettings) { + $brandingTable = Get-CippTable -tablename 'Config' + $BrandingSettings = Get-CIPPAzDataTableEntity @brandingTable -Filter "RowKey eq 'BrandingSettings'" + if ($BrandingSettings) { + $UserSettings | Add-Member -MemberType NoteProperty -Name 'BrandingSettings' -Value $BrandingSettings -Force | Out-Null + } + } $StatusCode = [HttpStatusCode]::OK $Results = $UserSettings } catch { From 7e5b8f4cc4fcb4fa0a941d33523e706bffe7b4a5 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 20 Jun 2025 00:43:21 +0200 Subject: [PATCH 143/160] stupid bug --- .../CIPP/Settings/Invoke-ExecBrandingSettings.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecBrandingSettings.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecBrandingSettings.ps1 index d606b6d28da0..3c31d4310d65 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecBrandingSettings.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecBrandingSettings.ps1 @@ -18,7 +18,7 @@ Function Invoke-ExecBrandingSettings { $Results = @{} try { - $Table = Get-CIPPTable -TableName Settings + $Table = Get-CIPPTable -TableName Config $Filter = "PartitionKey eq 'BrandingSettings' and RowKey eq 'BrandingSettings'" $BrandingConfig = Get-CIPPAzDataTableEntity @Table -Filter $Filter @@ -62,6 +62,7 @@ Function Invoke-ExecBrandingSettings { try { $ImageBytes = [Convert]::FromBase64String($Base64Data) if ($ImageBytes.Length -le 2097152) { + Write-Host 'updating logo' $BrandingConfig.logo = $Logo $Updated = $true } else { From 67fa371e192303441ace1a7a9e7d25e2a4ef0de2 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 20 Jun 2025 01:28:22 +0200 Subject: [PATCH 144/160] secure score --- .../CIPP/Settings/Invoke-ExecBrandingSettings.ps1 | 3 --- 1 file changed, 3 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecBrandingSettings.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecBrandingSettings.ps1 index 3c31d4310d65..3daab7856161 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecBrandingSettings.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecBrandingSettings.ps1 @@ -51,7 +51,6 @@ Function Invoke-ExecBrandingSettings { } else { $StatusCode = [HttpStatusCode]::BadRequest $Results = 'Error: Invalid color format. Please use hex format (e.g., #F77F00)' - break } } @@ -68,12 +67,10 @@ Function Invoke-ExecBrandingSettings { } else { $StatusCode = [HttpStatusCode]::BadRequest $Results = 'Error: Image size must be less than 2MB' - break } } catch { $StatusCode = [HttpStatusCode]::BadRequest $Results = 'Error: Invalid base64 image data' - break } } elseif ($Logo -eq $null -or $Logo -eq '') { $BrandingConfig.logo = $null From 5c78dfc694760b25d72fafc4aaa687a25fe2e8da Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 19 Jun 2025 21:04:38 -0400 Subject: [PATCH 145/160] Create Invoke-ListAdminPortalLicenses.ps1 --- .../Core/Invoke-ListAdminPortalLicenses.ps1 | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListAdminPortalLicenses.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListAdminPortalLicenses.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListAdminPortalLicenses.ps1 new file mode 100644 index 000000000000..d194984ff027 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListAdminPortalLicenses.ps1 @@ -0,0 +1,24 @@ +function Invoke-ListAdminPortalLicenses { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + CIPP.Core.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $TenantFilter = $Request.Query.tenantFilter + + try { + $AdminPortalLicenses = New-GraphGetRequest -scope 'https://admin.microsoft.com/.default' -TenantID $TenantFilter -Uri 'https://admin.microsoft.com/admin/api/tenant/accountSkus' + } catch { + Write-Warning 'Failed to get Admin Portal Licenses' + $AdminPortalLicenses = @() + } + + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = @($AdminPortalLicenses) + }) +} From 4e46f983e81a02141ace0e620fde5795fe0ac2a4 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 20 Jun 2025 00:07:39 -0400 Subject: [PATCH 146/160] remove timer function for audit log cleanup legacy function using office 365 management logs causing errors in the logbook due to missing consent fixes ticket 24563776345 --- CIPPTimers.json | 8 -------- ...tart-CIPPGraphSubscriptionCleanupTimer.ps1 | 19 ------------------- 2 files changed, 27 deletions(-) delete mode 100644 Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-CIPPGraphSubscriptionCleanupTimer.ps1 diff --git a/CIPPTimers.json b/CIPPTimers.json index 51b0debdbb9f..cbd1366a5363 100644 --- a/CIPPTimers.json +++ b/CIPPTimers.json @@ -80,14 +80,6 @@ "RunOnProcessor": true, "PreferredProcessor": "standards" }, - { - "Id": "5113c66d-c040-42df-9565-39dff90ddd55", - "Command": "Start-CIPPGraphSubscriptionCleanupTimer", - "Description": "Orchestrator to cleanup old Graph subscriptions", - "Cron": "0 0 0 * * *", - "Priority": 5, - "RunOnProcessor": true - }, { "Id": "97145a1d-28f0-4bb2-b929-5a43517d23cc", "Command": "Start-SchedulerOrchestrator", diff --git a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-CIPPGraphSubscriptionCleanupTimer.ps1 b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-CIPPGraphSubscriptionCleanupTimer.ps1 deleted file mode 100644 index 65d1ea7c29e1..000000000000 --- a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-CIPPGraphSubscriptionCleanupTimer.ps1 +++ /dev/null @@ -1,19 +0,0 @@ -function Start-CIPPGraphSubscriptionCleanupTimer { - <# - .SYNOPSIS - Remove CIPP Graph Subscriptions for all tenants except the partner tenant. - - .DESCRIPTION - Remove CIPP Graph Subscriptions for all tenants except the partner tenant. - #> - [CmdletBinding(SupportsShouldProcess = $true)] - param() - try { - $Tenants = Get-Tenants -IncludeAll | Where-Object { $_.customerId -ne $env:TenantID -and $_.Excluded -eq $false } - $Tenants | ForEach-Object { - if ($PSCmdlet.ShouldProcess($_.defaultDomainName, 'Remove-CIPPGraphSubscription')) { - Remove-CIPPGraphSubscription -cleanup $true -TenantFilter $_.defaultDomainName - } - } - } catch {} -} From bc720bdc80ba9d1dc136775e3b20118f69c2a841 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 20 Jun 2025 01:10:04 -0400 Subject: [PATCH 147/160] improve table cleanup --- .../Maintenance/Push-TableCleanupTask.ps1 | 67 +++++++++++++++ .../Timer Functions/Start-TableCleanup.ps1 | 84 +++++++------------ 2 files changed, 95 insertions(+), 56 deletions(-) create mode 100644 Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Maintenance/Push-TableCleanupTask.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Maintenance/Push-TableCleanupTask.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Maintenance/Push-TableCleanupTask.ps1 new file mode 100644 index 000000000000..180c7c06fda9 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Maintenance/Push-TableCleanupTask.ps1 @@ -0,0 +1,67 @@ +function Push-TableCleanupTask { + [CmdletBinding(SupportsShouldProcess = $true)] + param ( + [Parameter(Mandatory = $true)] + $Item + ) + + $Type = $Item.Type + Write-Information "#### Starting $($Type) task..." + if ($PSCmdlet.ShouldProcess('Start-TableCleanup', 'Starting Table Cleanup')) { + if ($Type -eq 'DeleteTable') { + $DeleteTables = $Item.Tables + foreach ($Table in $DeleteTables) { + try { + $Table = Get-CIPPTable -tablename $Table + if ($Table) { + Write-Information "Deleting table $($Table.Context.TableName)" + try { + Remove-AzDataTable -Context $Table.Context -Force + } catch { + #Write-LogMessage -API 'TableCleanup' -message "Failed to delete table $($Table.Context.TableName)" -sev Error -LogData (Get-CippException -Exception $_) + } + } + } catch { + Write-Information "Table $Table not found" + } + } + } elseif ($Type -eq 'CleanupRule') { + if ($Item.Where) { + $Where = [scriptblock]::Create($Item.Where) + } else { + $Where = { $true } + } + + $DataTableProps = $Item.DataTableProps | ConvertTo-Json | ConvertFrom-Json -AsHashtable + $Table = Get-CIPPTable -tablename $Item.TableName + $CleanupCompleted = $false + do { + Write-Information "Fetching entities from $($Item.TableName) with filter: $($DataTableProps.Filter)" + try { + $Entities = Get-AzDataTableEntity @Table @DataTableProps | Where-Object $Where + if ($Entities) { + Write-Information "Removing $($Entities.Count) entities from $($Item.TableName)" + try { + Remove-AzDataTableEntity @Table -Entity $Entities -Force + if ($DataTableProps.First -and $Entities.Count -lt $DataTableProps.First) { + $CleanupCompleted = $true + } + } catch { + Write-LogMessage -API 'TableCleanup' -message "Failed to remove entities from $($Item.TableName)" -sev Error -LogData (Get-CippException -Exception $_) + $CleanupCompleted = $true + } + } else { + Write-Information "No entities found for cleanup in $($Item.TableName)" + $CleanupCompleted = $true + } + } catch { + Write-Warning "Failed to fetch entities from $($Item.TableName): $($_.Exception.Message)" + $CleanupCompleted = $true + } + } while (!$CleanupCompleted) + } else { + Write-Warning "Unknown task type: $Type" + } + } + Write-Information "#### $($Type) task complete" +} diff --git a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-TableCleanup.ps1 b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-TableCleanup.ps1 index 864e1017074d..177e67cdb378 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-TableCleanup.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-TableCleanup.ps1 @@ -6,97 +6,69 @@ function Start-TableCleanup { [CmdletBinding(SupportsShouldProcess = $true)] param() - $CleanupRules = @( + $Batch = @( @{ + FunctionName = 'TableCleanupTask' + Type = 'CleanupRule' + TableName = 'webhookTable' DataTableProps = @{ - Context = (Get-CIPPTable -tablename 'webhookTable').Context Property = @('PartitionKey', 'RowKey', 'ETag', 'Resource') + First = 1000 } Where = "`$_.Resource -match '^Audit'" } @{ + FunctionName = 'TableCleanupTask' + Type = 'CleanupRule' + TableName = 'AuditLogSearches' DataTableProps = @{ - Context = (Get-CIPPTable -tablename 'AuditLogSearches').Context Filter = "PartitionKey eq 'Search' and Timestamp lt datetime'$((Get-Date).AddHours(-12).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ'))'" First = 10000 Property = @('PartitionKey', 'RowKey', 'ETag') } } @{ + FunctionName = 'TableCleanupTask' + Type = 'CleanupRule' + TableName = 'CippFunctionStats' DataTableProps = @{ - Context = (Get-CIPPTable -tablename 'CippFunctionStats').Context Filter = "PartitionKey eq 'Durable' and Timestamp lt datetime'$((Get-Date).AddDays(-7).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ'))'" First = 10000 Property = @('PartitionKey', 'RowKey', 'ETag') } } @{ + FunctionName = 'TableCleanupTask' + Type = 'CleanupRule' + TableName = 'CippQueue' DataTableProps = @{ - Context = (Get-CIPPTable -tablename 'CippQueue').Context Filter = "PartitionKey eq 'CippQueue' and Timestamp lt datetime'$((Get-Date).AddDays(-7).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ'))'" First = 10000 Property = @('PartitionKey', 'RowKey', 'ETag') } } @{ + FunctionName = 'TableCleanupTask' + Type = 'CleanupRule' + TableName = 'CippQueueTasks' DataTableProps = @{ - Context = (Get-CIPPTable -tablename 'CippQueueTasks').Context Filter = "PartitionKey eq 'Task' and Timestamp lt datetime'$((Get-Date).AddDays(-7).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ'))'" First = 10000 Property = @('PartitionKey', 'RowKey', 'ETag') } } - ) - - $DeleteTables = @( - 'knownlocationdb' - ) - - if ($PSCmdlet.ShouldProcess('Start-TableCleanup', 'Starting Table Cleanup')) { - foreach ($Table in $DeleteTables) { - try { - $Table = Get-CIPPTable -tablename $Table - if ($Table) { - Write-Information "Deleting table $($Table.Context.TableName)" - try { - Remove-AzDataTable -Context $Table.Context -Force - } catch { - #Write-LogMessage -API 'TableCleanup' -message "Failed to delete table $($Table.Context.TableName)" -sev Error -LogData (Get-CippException -Exception $_) - } - } - } catch { - Write-Information "Table $Table not found" - } + @{ + FunctionName = 'TableCleanupTask' + Type = 'DeleteTable' + Tables = @('knownlocationdb') } + ) - Write-Information 'Starting table cleanup' - foreach ($Rule in $CleanupRules) { - if ($Rule.Where) { - $Where = [scriptblock]::Create($Rule.Where) - } else { - $Where = { $true } - } - $DataTableProps = $Rule.DataTableProps - - $CleanupCompleted = $false - do { - $Entities = Get-AzDataTableEntity @DataTableProps | Where-Object $Where - if ($Entities) { - Write-Information "Removing $($Entities.Count) entities from $($Rule.DataTableProps.Context.TableName)" - try { - Remove-AzDataTableEntity -Context $DataTableProps.Context -Entity $Entities -Force - if ($DataTableProps.First -and $Entities.Count -lt $DataTableProps.First) { - $CleanupCompleted = $true - } - } catch { - Write-LogMessage -API 'TableCleanup' -message "Failed to remove entities from $($DataTableProps.Context.TableName)" -sev Error -LogData (Get-CippException -Exception $_) - $CleanupCompleted = $true - } - } else { - $CleanupCompleted = $true - } - } while (!$CleanupCompleted) - } - Write-Information 'Table cleanup complete' + $InputObject = @{ + Batch = @($Batch) + OrchestratorName = 'TableCleanup' + SkipLog = $true } + + Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) } From ed0d2504a4dd91f7298543530f8ec4adf0624144 Mon Sep 17 00:00:00 2001 From: Esco Date: Fri, 20 Jun 2025 10:14:44 +0200 Subject: [PATCH 148/160] fix: PasswordExpireDisabled ignore notVerified --- .../Standards/Invoke-CIPPStandardPasswordExpireDisabled.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPasswordExpireDisabled.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPasswordExpireDisabled.ps1 index bd3e581bbd97..a801f3a32244 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPasswordExpireDisabled.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPasswordExpireDisabled.ps1 @@ -34,7 +34,8 @@ function Invoke-CIPPStandardPasswordExpireDisabled { param($Tenant, $Settings) $GraphRequest = New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/domains' -tenantid $Tenant - $DomainsWithoutPassExpire = $GraphRequest | Where-Object -Property passwordValidityPeriodInDays -NE '2147483647' + $DomainsWithoutPassExpire = $GraphRequest | + Where-Object { $_.isVerified -eq $true -and $_.passwordValidityPeriodInDays -ne 2147483647 } if ($Settings.remediate -eq $true) { From 9aa093e9a64d43d7416d67cfc62bae3e5f36d5b6 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 20 Jun 2025 11:40:06 +0200 Subject: [PATCH 149/160] local dev auth fix --- .../CIPP/Setup/Invoke-ExecUpdateRefreshToken.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecUpdateRefreshToken.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecUpdateRefreshToken.ps1 index 6a8254483607..7afce7c01f65 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecUpdateRefreshToken.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecUpdateRefreshToken.ps1 @@ -20,8 +20,9 @@ Function Invoke-ExecUpdateRefreshToken { if ($env:AzureWebJobsStorage -eq 'UseDevelopmentStorage=true') { $DevSecretsTable = Get-CIPPTable -tablename 'DevSecrets' $Secret = Get-CIPPAzDataTableEntity @DevSecretsTable -Filter "PartitionKey eq 'Secret' and RowKey eq 'Secret'" + if ($env:TenantID -eq $Request.body.tenantId) { - $Secret.RefreshToken = $Request.body.RefreshToken + $Secret | Add-Member -MemberType NoteProperty -Name 'RefreshToken' -Value $Request.body.refreshtoken -Force } else { Write-Host "$($env:TenantID) does not match $($Request.body.tenantId)" $name = $Request.body.tenantId -replace '-', '_' From 33ef115f662bd53ee9cdbc3f3d062d86aeb361f1 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 20 Jun 2025 12:33:50 +0200 Subject: [PATCH 150/160] branding update --- .../Settings/Invoke-ExecBrandingSettings.ps1 | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecBrandingSettings.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecBrandingSettings.ps1 index 3daab7856161..ab721161c8c1 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecBrandingSettings.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecBrandingSettings.ps1 @@ -15,7 +15,7 @@ Function Invoke-ExecBrandingSettings { Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' $StatusCode = [HttpStatusCode]::OK - $Results = @{} + @{} try { $Table = Get-CIPPTable -TableName Config @@ -33,9 +33,9 @@ Function Invoke-ExecBrandingSettings { $Action = if ($Request.Body.Action) { $Request.Body.Action } else { $Request.Query.Action } - switch ($Action) { + $Results = switch ($Action) { 'Get' { - $Results = @{ + @{ colour = $BrandingConfig.colour logo = $BrandingConfig.logo } @@ -50,7 +50,7 @@ Function Invoke-ExecBrandingSettings { $Updated = $true } else { $StatusCode = [HttpStatusCode]::BadRequest - $Results = 'Error: Invalid color format. Please use hex format (e.g., #F77F00)' + 'Error: Invalid color format. Please use hex format (e.g., #F77F00)' } } @@ -62,18 +62,18 @@ Function Invoke-ExecBrandingSettings { $ImageBytes = [Convert]::FromBase64String($Base64Data) if ($ImageBytes.Length -le 2097152) { Write-Host 'updating logo' - $BrandingConfig.logo = $Logo + $BrandingConfig | Add-Member -MemberType NoteProperty -Name 'logo' -Value $Logo -Force $Updated = $true } else { $StatusCode = [HttpStatusCode]::BadRequest - $Results = 'Error: Image size must be less than 2MB' + 'Error: Image size must be less than 2MB' } } catch { $StatusCode = [HttpStatusCode]::BadRequest - $Results = 'Error: Invalid base64 image data' + 'Error: Invalid base64 image data: ' + $_.Exception.Message } } elseif ($Logo -eq $null -or $Logo -eq '') { - $BrandingConfig.logo = $null + $BrandingConfig | Add-Member -MemberType NoteProperty -Name 'logo' -Value $null -Force $Updated = $true } } @@ -84,10 +84,10 @@ Function Invoke-ExecBrandingSettings { Add-CIPPAzDataTableEntity @Table -Entity $BrandingConfig -Force | Out-Null Write-LogMessage -API $APIName -tenant 'Global' -headers $Request.Headers -message 'Updated branding settings' -Sev 'Info' - $Results = 'Successfully updated branding settings' + 'Successfully updated branding settings' } else { $StatusCode = [HttpStatusCode]::BadRequest - $Results = 'Error: No valid branding data provided' + 'Error: No valid branding data provided' } } 'Reset' { @@ -100,17 +100,17 @@ Function Invoke-ExecBrandingSettings { Add-CIPPAzDataTableEntity @Table -Entity $DefaultConfig -Force | Out-Null Write-LogMessage -API $APIName -tenant 'Global' -headers $Request.Headers -message 'Reset branding settings to defaults' -Sev 'Info' - $Results = 'Successfully reset branding settings to defaults' + 'Successfully reset branding settings to defaults' } default { $StatusCode = [HttpStatusCode]::BadRequest - $Results = 'Error: Invalid action specified' + 'Error: Invalid action specified' } } } catch { Write-LogMessage -API $APIName -tenant 'Global' -headers $Request.Headers -message "Branding Settings API failed: $($_.Exception.Message)" -Sev 'Error' $StatusCode = [HttpStatusCode]::InternalServerError - $Results = "Failed to process branding settings: $($_.Exception.Message)" + "Failed to process branding settings: $($_.Exception.Message)" } $body = [pscustomobject]@{'Results' = $Results } From 3e067c814b156d7594980788a485bc47c67f55ac Mon Sep 17 00:00:00 2001 From: Esco Date: Fri, 20 Jun 2025 14:52:20 +0200 Subject: [PATCH 151/160] fix: added missing alerts --- .../Standards/Invoke-CIPPStandardExcludedfileExt.ps1 | 1 + .../Invoke-CIPPStandardQuarantineRequestAlert.ps1 | 4 +++- .../Standards/Invoke-CIPPStandardSPAzureB2B.ps1 | 4 +++- .../Standards/Invoke-CIPPStandardSPDirectSharing.ps1 | 12 +++++++----- .../Invoke-CIPPStandardSPDisableLegacyWorkflows.ps1 | 6 ++++-- .../Invoke-CIPPStandardSPDisallowInfectedFiles.ps1 | 4 +++- .../Invoke-CIPPStandardSPEmailAttestation.ps1 | 4 +++- .../Invoke-CIPPStandardSPExternalUserExpiration.ps1 | 4 +++- 8 files changed, 27 insertions(+), 12 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExcludedfileExt.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExcludedfileExt.ps1 index 34cbf330fed0..bec921cca4cf 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExcludedfileExt.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExcludedfileExt.ps1 @@ -69,6 +69,7 @@ function Invoke-CIPPStandardExcludedfileExt { if ($Settings.alert -eq $true) { if ($MissingExclusions) { + Write-StandardsAlert -message 'Exclude File Extensions from Syncing missing some extensions.' -object $MissingExclusions -tenant $Tenant -standardName 'ExcludedfileExt' -standardId $Settings.standardId Write-LogMessage -API 'Standards' -tenant $tenant -message "Excluded synced files does not contain $($MissingExclusions -join ',')" -sev Alert } else { Write-LogMessage -API 'Standards' -tenant $tenant -message "Excluded synced files contains $($Settings.ext)" -sev Info diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineRequestAlert.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineRequestAlert.ps1 index 088cfe4f5ffa..782d847ec8a1 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineRequestAlert.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineRequestAlert.ps1 @@ -78,7 +78,9 @@ function Invoke-CIPPStandardQuarantineRequestAlert { if ($StateIsCorrect -eq $true) { Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'Quarantine Request Alert is enabled' -sev Info } else { - Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'Quarantine Request Alert is disabled' -sev Info + $Message = 'Quarantine Request Alert is not enabled.' + Write-StandardsAlert -message $Message -object $CurrentState -tenant $Tenant -standardName 'QuarantineRequestAlerts' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message $Message -sev Info } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPAzureB2B.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPAzureB2B.ps1 index 51d24c66deff..016f4697e400 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPAzureB2B.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPAzureB2B.ps1 @@ -58,7 +58,9 @@ function Invoke-CIPPStandardSPAzureB2B { if ($StateIsCorrect -eq $true) { Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'SharePoint Azure B2B is enabled' -Sev Info } else { - Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'SharePoint Azure B2B is not enabled' -Sev Alert + $Message = 'SharePoint Azure B2B is not enabled.' + Write-StandardsAlert -message $Message -object $CurrentState -tenant $Tenant -standardName 'SPAzureB2B' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message $Message -Sev Alert } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDirectSharing.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDirectSharing.ps1 index b6d529cfeac5..e91952957966 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDirectSharing.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDirectSharing.ps1 @@ -39,7 +39,7 @@ function Invoke-CIPPStandardSPDirectSharing { if ($Settings.remediate -eq $true) { if ($StateIsCorrect -eq $true) { - Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'SharePoint Sharing Restriction is already enabled' -Sev Info + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'SharePoint Default Direct Sharing is already enabled' -Sev Info } else { $Properties = @{ DefaultSharingLinkType = 1 @@ -47,19 +47,21 @@ function Invoke-CIPPStandardSPDirectSharing { try { Get-CIPPSPOTenant -TenantFilter $Tenant | Set-CIPPSPOTenant -Properties $Properties - Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'Successfully set the SharePoint Sharing Restriction to Direct' -Sev Info + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'Successfully set the SharePoint Default Direct Sharing to Direct' -Sev Info } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Failed to set the SharePoint Sharing Restriction to Direct. Error: $ErrorMessage" -Sev Error + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Failed to set the SharePoint Default Direct Sharing to Direct. Error: $ErrorMessage" -Sev Error } } } if ($Settings.alert -eq $true) { if ($StateIsCorrect -eq $true) { - Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'SharePoint Sharing Restriction is enabled' -Sev Info + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'SharePoint Direct Sharing is enabled' -Sev Info } else { - Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'SharePoint Sharing Restriction is not enabled' -Sev Alert + $Message = 'SharePoint Default Direct Sharing is not enabled.' + Write-StandardsAlert -message $Message -object $CurrentState -tenant $Tenant -standardName 'SPDirectSharing' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message $Message -Sev Alert } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDisableLegacyWorkflows.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDisableLegacyWorkflows.ps1 index 3093949eceab..57a4f292367c 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDisableLegacyWorkflows.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDisableLegacyWorkflows.ps1 @@ -57,9 +57,11 @@ function Invoke-CIPPStandardSPDisableLegacyWorkflows { if ($Settings.alert -eq $true) { if ($StateIsCorrect -eq $true) { - Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'Legacy Workflows are disabled' -Sev Info + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'SharePoint Legacy Workflows are disabled' -Sev Info } else { - Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'Legacy Workflows are enabled' -Sev Info + $Message = 'SharePoint Legacy Workflows is not disabled.' + Write-StandardsAlert -message $Message -object $CurrentState -tenant $Tenant -standardName 'SPDisableLegacyWorkflows' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message $Message -Sev Info } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDisallowInfectedFiles.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDisallowInfectedFiles.ps1 index 60278d1c2e11..c61ec77268fb 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDisallowInfectedFiles.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDisallowInfectedFiles.ps1 @@ -59,7 +59,9 @@ function Invoke-CIPPStandardSPDisallowInfectedFiles { if ($StateIsCorrect -eq $true) { Write-LogMessage -API 'Standards' -tenant $tenant -Message 'Downloading SharePoint infected files are disallowed.' -Sev Info } else { - Write-LogMessage -API 'Standards' -tenant $tenant -Message 'Downloading SharePoint infected files are allowed.' -Sev Alert + $Message = 'Downloading SharePoint infected files is not set to the desired value.' + Write-StandardsAlert -message $Message -object $CurrentState -tenant $Tenant -standardName 'SPDisallowInfectedFiles' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $tenant -Message $Message -Sev Alert } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPEmailAttestation.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPEmailAttestation.ps1 index 7c0ed3fb7d4d..c26879a60c2b 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPEmailAttestation.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPEmailAttestation.ps1 @@ -66,7 +66,9 @@ function Invoke-CIPPStandardSPEmailAttestation { if ($StateIsCorrect -eq $true) { Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'Re-authentication with verification code is restricted.' -Sev Info } else { - Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'Re-authentication with verification code is not restricted.' -Sev Alert + $Message = 'Re-authentication with verification code is not set to the desired value.' + Write-StandardsAlert -message $Message -object $CurrentState -tenant $Tenant -standardName 'SPEmailAttestation' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message $Message -Sev Alert } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPExternalUserExpiration.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPExternalUserExpiration.ps1 index 7fc1b7fd1c14..7db6ed6a53e1 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPExternalUserExpiration.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPExternalUserExpiration.ps1 @@ -61,7 +61,9 @@ function Invoke-CIPPStandardSPExternalUserExpiration { if ($StateIsCorrect -eq $true) { Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'External User Expiration is enabled' -Sev Info } else { - Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'External User Expiration is not enabled' -Sev Alert + $Message = 'External User Expiration is not set to the desired value.' + Write-StandardsAlert -message $Message -object $CurrentState -tenant $Tenant -standardName 'SPExternalUserExpiration' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message $Message -Sev Alert } } From 99a23b75e46e8fde70df408418b2528616e12c96 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 20 Jun 2025 10:55:44 -0400 Subject: [PATCH 152/160] ensure the correct base role is selected --- .../Public/Authentication/Test-CIPPAccess.ps1 | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 b/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 index 004b327c21a5..5f10e71d0f6f 100644 --- a/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 +++ b/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 @@ -123,11 +123,18 @@ function Test-CIPPAccess { } $BaseRole = $null - foreach ($Role in $BaseRoles.PSObject.Properties) { - foreach ($UserRole in $User.userRoles) { - if ($Role.Name -eq $UserRole) { - $BaseRole = $Role - break + + if ($User.userRoles -contains 'superadmin') { + $BaseRole = 'superadmin' + } elseif ($User.userRoles -contains 'admin') { + $BaseRole = 'admin' + } else { + foreach ($Role in $BaseRoles.PSObject.Properties) { + foreach ($UserRole in $User.userRoles) { + if ($Role.Name -eq $UserRole) { + $BaseRole = $Role + break + } } } } From 1009b5ff16437622036c188d429062b4de245426 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 20 Jun 2025 10:59:59 -0400 Subject: [PATCH 153/160] Update Test-CIPPAccess.ps1 --- .../Public/Authentication/Test-CIPPAccess.ps1 | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 b/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 index 5f10e71d0f6f..af8a7ff8dc2f 100644 --- a/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 +++ b/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 @@ -125,19 +125,19 @@ function Test-CIPPAccess { $BaseRole = $null if ($User.userRoles -contains 'superadmin') { - $BaseRole = 'superadmin' + $User.userRoles = @('superadmin') } elseif ($User.userRoles -contains 'admin') { - $BaseRole = 'admin' - } else { - foreach ($Role in $BaseRoles.PSObject.Properties) { - foreach ($UserRole in $User.userRoles) { - if ($Role.Name -eq $UserRole) { - $BaseRole = $Role - break - } + $User.userRoles = @('admin') + } + foreach ($Role in $BaseRoles.PSObject.Properties) { + foreach ($UserRole in $User.userRoles) { + if ($Role.Name -eq $UserRole) { + $BaseRole = $Role + break } } } + } # Check base role permissions before continuing to custom roles From 5817139cb14efa81eda5c7ee89b77155bf61735f Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 20 Jun 2025 11:32:12 -0400 Subject: [PATCH 154/160] add logdata to bec remediate log --- .../Identity/Administration/Users/Invoke-ExecBECRemediate.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBECRemediate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBECRemediate.ps1 index d647d7fc5dff..51d8203a77c6 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBECRemediate.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBECRemediate.ps1 @@ -1,6 +1,6 @@ using namespace System.Net -Function Invoke-ExecBECRemediate { +function Invoke-ExecBECRemediate { <# .FUNCTIONALITY Entrypoint @@ -54,7 +54,7 @@ Function Invoke-ExecBECRemediate { "Failed to disable $RuleFailed Inbox Rules for $Username" } $StatusCode = [HttpStatusCode]::OK - Write-LogMessage -API 'BECRemediate' -tenant $TenantFilter -message "Executed Remediation for $Username" -sev 'Info' + Write-LogMessage -API 'BECRemediate' -tenant $TenantFilter -message "Executed Remediation for $Username" -sev 'Info' -LogData @($Results) } catch { $ErrorMessage = Get-CippException -Exception $_ From efaee28e2bf2272cb3b31a7eebd87adaad710ce5 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 20 Jun 2025 17:54:50 +0200 Subject: [PATCH 155/160] fix for normalized error --- Modules/CIPPCore/Public/GraphHelper/Get-NormalizedError.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/GraphHelper/Get-NormalizedError.ps1 b/Modules/CIPPCore/Public/GraphHelper/Get-NormalizedError.ps1 index 7c230e6e8d0b..75b05c0fc184 100644 --- a/Modules/CIPPCore/Public/GraphHelper/Get-NormalizedError.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/Get-NormalizedError.ps1 @@ -43,7 +43,7 @@ function Get-NormalizedError { 'Response status code does not indicate success: 400 (Bad Request).' { 'Error 400 occured. There is an issue with the token configuration for this tenant. Please perform an access check' } '*Microsoft.Skype.Sync.Pstn.Tnm.Common.Http.HttpResponseException*' { 'Could not connect to Teams Admin center - Tenant might be missing a Teams license' } '*Provide valid credential.*' { 'Error 400: There is an issue with your Exchange Token configuration. Please perform an access check for this tenant' } - '*This indicate that a subscription within the tenant has lapsed*' { 'There is subscription for this service available, Check licensing information.' } + '*This indicate that a subscription within the tenant has lapsed*' { 'There is no subscription for this service available, Check licensing information.' } '*User was not found.*' { 'The relationship between this tenant and the partner has been dissolved from the tenant side.' } '*AADSTS50020*' { 'AADSTS50020: The user you have used for your Secure Application Model is a guest in this tenant, or your are using GDAP and have not added the user to the correct group. Please delete the guest user to gain access to this tenant' } '*AADSTS50177' { 'AADSTS50177: The user you have used for your Secure Application Model is a guest in this tenant, or your are using GDAP and have not added the user to the correct group. Please delete the guest user to gain access to this tenant' } From 487a2cef23d86ea437159b0364a595314bc8e28d Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 20 Jun 2025 11:57:17 -0400 Subject: [PATCH 156/160] update error status code --- Modules/CippEntrypoints/CippEntrypoints.psm1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CippEntrypoints/CippEntrypoints.psm1 b/Modules/CippEntrypoints/CippEntrypoints.psm1 index 76def2143679..546ca1cebb7a 100644 --- a/Modules/CippEntrypoints/CippEntrypoints.psm1 +++ b/Modules/CippEntrypoints/CippEntrypoints.psm1 @@ -61,7 +61,7 @@ function Receive-CippHttpTrigger { } catch { Write-Warning "Exception occurred on HTTP trigger ($FunctionName): $($_.Exception.Message)" Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ - StatusCode = [HttpStatusCode]::Forbidden + StatusCode = [HttpStatusCode]::InternalServerError Body = $_.Exception.Message }) } From b9e62a40e6239afbc5c20b3d3787fafd19e326d3 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 20 Jun 2025 18:03:24 +0200 Subject: [PATCH 157/160] version up --- version_latest.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version_latest.txt b/version_latest.txt index 215aacb45236..8104cabd36fb 100644 --- a/version_latest.txt +++ b/version_latest.txt @@ -1 +1 @@ -8.0.3 +8.1.0 From 1c4d8dc4d668cfcb96d1a5825c3b553a05601f38 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 20 Jun 2025 18:15:43 +0200 Subject: [PATCH 158/160] Resolves an issue with a parameter that is required for operation when a request is sent using an https api to the sharepoint rest api this commit resolves that issue and fixes the problem --- .../Public/Alerts/Get-CIPPAlertSharepointQuota.ps1 | 2 +- .../Invoke-ListSharepointAdminUrl.ps1 | 2 +- .../Invoke-ListSharepointQuota.ps1 | 2 +- Modules/CIPPCore/Public/Get-CIPPSPOTenant.ps1 | 2 +- .../Public/GraphHelper/Get-SharePointAdminLink.ps1 | 14 +++++++------- Modules/CIPPCore/Public/New-CIPPSharepointSite.ps1 | 2 +- .../Public/Request-CIPPSPOPersonalSite.ps1 | 3 +-- Modules/CIPPCore/Public/Set-CIPPSPOTenant.ps1 | 2 +- .../CIPPCore/Public/Set-CIPPSharePointPerms.ps1 | 3 +-- 9 files changed, 15 insertions(+), 17 deletions(-) diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSharepointQuota.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSharepointQuota.ps1 index 2460a77905f5..79082f927d66 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSharepointQuota.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSharepointQuota.ps1 @@ -11,7 +11,7 @@ function Get-CIPPAlertSharepointQuota { $TenantFilter ) Try { - $SharePointInfo = Get-SharePointAdminLink -Public $false + $SharePointInfo = Get-SharePointAdminLink -Public $false -tenantFilter $TenantFilter $sharepointQuota = (New-GraphGetRequest -scope "$($SharePointInfo.AdminUrl)/.default" -tenantid $TenantFilter -uri "$($SharePointInfo.AdminUrl)/_api/StorageQuotas()?api-version=1.3.2").value } catch { return diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointAdminUrl.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointAdminUrl.ps1 index 31bf6a2aabf6..d486b5eec8cb 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointAdminUrl.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointAdminUrl.ps1 @@ -19,7 +19,7 @@ function Invoke-ListSharepointAdminUrl { if ($Tenant.SharepointAdminUrl) { $AdminUrl = $Tenant.SharepointAdminUrl } else { - $SharePointInfo = Get-SharePointAdminLink -Public $false + $SharePointInfo = Get-SharePointAdminLink -Public $false -tenantFilter $TenantFilter $Tenant | Add-Member -MemberType NoteProperty -Name SharepointAdminUrl -Value $SharePointInfo.AdminUrl $Table = Get-CIPPTable -TableName 'Tenants' Add-CIPPAzDataTableEntity @Table -Entity $Tenant -Force diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointQuota.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointQuota.ps1 index c2ca4eea4135..d132c77767a1 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointQuota.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointQuota.ps1 @@ -21,7 +21,7 @@ Function Invoke-ListSharepointQuota { $UsedStoragePercentage = 'Not Supported' } else { try { - $SharePointInfo = Get-SharePointAdminLink -Public $false + $SharePointInfo = Get-SharePointAdminLink -Public $false -tenantFilter $TenantFilter $SharePointQuota = (New-GraphGetRequest -scope "$($SharePointInfo.AdminUrl)/.default" -tenantid $TenantFilter -uri "$($SharePointInfo.AdminUrl)/_api/StorageQuotas()?api-version=1.3.2").value | Sort-Object -Property GeoUsedStorageMB -Descending | Select-Object -First 1 if ($SharePointQuota) { diff --git a/Modules/CIPPCore/Public/Get-CIPPSPOTenant.ps1 b/Modules/CIPPCore/Public/Get-CIPPSPOTenant.ps1 index 16efdfa588ed..0435eabdaf13 100644 --- a/Modules/CIPPCore/Public/Get-CIPPSPOTenant.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPSPOTenant.ps1 @@ -8,7 +8,7 @@ function Get-CIPPSPOTenant { if (!$SharepointPrefix) { # get sharepoint admin site - $SharePointInfo = Get-SharePointAdminLink -Public $false + $SharePointInfo = Get-SharePointAdminLink -Public $false -tenantFilter $TenantFilter $tenantName = $SharePointInfo.TenantName $AdminUrl = $SharePointInfo.AdminUrl } else { diff --git a/Modules/CIPPCore/Public/GraphHelper/Get-SharePointAdminLink.ps1 b/Modules/CIPPCore/Public/GraphHelper/Get-SharePointAdminLink.ps1 index 9e471ec8503e..43c7326489be 100644 --- a/Modules/CIPPCore/Public/GraphHelper/Get-SharePointAdminLink.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/Get-SharePointAdminLink.ps1 @@ -4,7 +4,7 @@ function Get-SharePointAdminLink { Internal #> [CmdletBinding()] - param ($Public) + param ($Public, $TenantFilter) if ($Public) { # Do it through domain discovery, unreliable @@ -42,10 +42,10 @@ function Get-SharePointAdminLink { # Get the onmicrosoft.com domain from the response $TenantDomains = $Response.Envelope.body.GetFederationInformationResponseMessage.response.Domains.Domain | Sort-Object - $OnMicrosoftDomains = $TenantDomains | Where-Object { $_ -like "*.onmicrosoft.com" } + $OnMicrosoftDomains = $TenantDomains | Where-Object { $_ -like '*.onmicrosoft.com' } if ($OnMicrosoftDomains.Count -eq 0) { - throw "Could not find onmicrosoft.com domain through autodiscover" + throw 'Could not find onmicrosoft.com domain through autodiscover' } elseif ($OnMicrosoftDomains.Count -gt 1) { throw "Multiple onmicrosoft.com domains found through autodiscover. Cannot determine the correct one: $($OnMicrosoftDomains -join ', ')" } else { @@ -61,8 +61,8 @@ function Get-SharePointAdminLink { # Return object with all needed properties return [PSCustomObject]@{ - AdminUrl = "https://$tenantName-admin.sharepoint.com" - TenantName = $tenantName - SharePointUrl = "https://$tenantName.sharepoint.com" + AdminUrl = "https://$tenantName-admin.sharepoint.com" + TenantName = $tenantName + SharePointUrl = "https://$tenantName.sharepoint.com" } -} \ No newline at end of file +} diff --git a/Modules/CIPPCore/Public/New-CIPPSharepointSite.ps1 b/Modules/CIPPCore/Public/New-CIPPSharepointSite.ps1 index 7c1074e4241d..cc5f7a3a1203 100644 --- a/Modules/CIPPCore/Public/New-CIPPSharepointSite.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPSharepointSite.ps1 @@ -66,7 +66,7 @@ function New-CIPPSharepointSite { $Headers ) - $SharePointInfo = Get-SharePointAdminLink -Public $false + $SharePointInfo = Get-SharePointAdminLink -Public $false -tenantFilter $TenantFilter $SitePath = $SiteName -replace ' ' -replace '[^A-Za-z0-9-]' $SiteUrl = "https://$($SharePointInfo.TenantName).sharepoint.com/sites/$SitePath" diff --git a/Modules/CIPPCore/Public/Request-CIPPSPOPersonalSite.ps1 b/Modules/CIPPCore/Public/Request-CIPPSPOPersonalSite.ps1 index dfee53d3e9c9..46afa2f3d333 100644 --- a/Modules/CIPPCore/Public/Request-CIPPSPOPersonalSite.ps1 +++ b/Modules/CIPPCore/Public/Request-CIPPSPOPersonalSite.ps1 @@ -37,8 +37,7 @@ function Request-CIPPSPOPersonalSite { "@ - $SharePointInfo = Get-SharePointAdminLink -Public $false - + $SharePointInfo = Get-SharePointAdminLink -Public $false -tenantFilter $TenantFilter try { $Request = New-GraphPostRequest -scope "$($SharePointInfo.AdminUrl)/.default" -tenantid $TenantFilter -Uri "$($SharePointInfo.AdminUrl)/_vti_bin/client.svc/ProcessQuery" -Type POST -Body $XML -ContentType 'text/xml' if (!$Request.IsComplete) { throw } diff --git a/Modules/CIPPCore/Public/Set-CIPPSPOTenant.ps1 b/Modules/CIPPCore/Public/Set-CIPPSPOTenant.ps1 index 57732fbbfdb1..c5c6db30d5da 100644 --- a/Modules/CIPPCore/Public/Set-CIPPSPOTenant.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPSPOTenant.ps1 @@ -44,7 +44,7 @@ function Set-CIPPSPOTenant { process { if (!$SharepointPrefix) { # get sharepoint admin site - $SharePointInfo = Get-SharePointAdminLink -Public $false + $SharePointInfo = Get-SharePointAdminLink -Public $false -tenantFilter $TenantFilter $AdminUrl = $SharePointInfo.AdminUrl } else { $tenantName = $SharepointPrefix diff --git a/Modules/CIPPCore/Public/Set-CIPPSharePointPerms.ps1 b/Modules/CIPPCore/Public/Set-CIPPSharePointPerms.ps1 index 060dbd8076ca..17bf2358d2de 100644 --- a/Modules/CIPPCore/Public/Set-CIPPSharePointPerms.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPSharePointPerms.ps1 @@ -21,8 +21,7 @@ function Set-CIPPSharePointPerms { $URL = (New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/users/$($UserId)/Drives" -asapp $true -tenantid $TenantFilter).WebUrl } - $SharePointInfo = Get-SharePointAdminLink -Public $false - + $SharePointInfo = Get-SharePointAdminLink -Public $false -tenantFilter $TenantFilter $XML = @" From 663a19094fc871a8ec4d04dee66c59bfe115ac15 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 20 Jun 2025 18:27:45 +0200 Subject: [PATCH 159/160] add extra headers to sharepoint --- .../Teams-Sharepoint/Invoke-ListSharepointQuota.ps1 | 5 ++++- .../CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 | 9 +++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointQuota.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointQuota.ps1 index d132c77767a1..9f9db254c6d1 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointQuota.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointQuota.ps1 @@ -22,7 +22,10 @@ Function Invoke-ListSharepointQuota { } else { try { $SharePointInfo = Get-SharePointAdminLink -Public $false -tenantFilter $TenantFilter - $SharePointQuota = (New-GraphGetRequest -scope "$($SharePointInfo.AdminUrl)/.default" -tenantid $TenantFilter -uri "$($SharePointInfo.AdminUrl)/_api/StorageQuotas()?api-version=1.3.2").value | Sort-Object -Property GeoUsedStorageMB -Descending | Select-Object -First 1 + $extraHeaders = @{ + 'Accept' = 'application/json' + } + $SharePointQuota = (New-GraphGetRequest -extraHeaders $extraHeaders -scope "$($SharePointInfo.AdminUrl)/.default" -tenantid $TenantFilter -uri "$($SharePointInfo.AdminUrl)/_api/StorageQuotas()?api-version=1.3.2") | Sort-Object -Property GeoUsedStorageMB -Descending | Select-Object -First 1 if ($SharePointQuota) { $UsedStoragePercentage = [int](($SharePointQuota.GeoUsedStorageMB / $SharePointQuota.TenantStorageMB) * 100) diff --git a/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 index 9383f7bf4245..12bab410eeee 100644 --- a/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 @@ -15,7 +15,8 @@ function New-GraphGetRequest { $Caller, [switch]$ComplexFilter, [switch]$CountOnly, - [switch]$IncludeResponseHeaders + [switch]$IncludeResponseHeaders, + [hashtable]$extraHeaders ) if ($NoAuthCheck -eq $false) { @@ -35,7 +36,11 @@ function New-GraphGetRequest { $headers['ConsistencyLevel'] = 'eventual' } $nextURL = $uri - + if ($extraHeaders) { + foreach ($key in $extraHeaders.Keys) { + $headers[$key] = $extraHeaders[$key] + } + } # Track consecutive Graph API failures $TenantsTable = Get-CippTable -tablename Tenants $Filter = "PartitionKey eq 'Tenants' and (defaultDomainName eq '{0}' or customerId eq '{0}')" -f $tenantid From a06b4619d5702972a516ad84c75090df5c84d0c1 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 20 Jun 2025 18:30:34 +0200 Subject: [PATCH 160/160] final fix for sharepoint stuff --- .../CIPPCore/Public/Alerts/Get-CIPPAlertSharepointQuota.ps1 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSharepointQuota.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSharepointQuota.ps1 index 79082f927d66..2de5890c7f85 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSharepointQuota.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSharepointQuota.ps1 @@ -12,7 +12,10 @@ function Get-CIPPAlertSharepointQuota { ) Try { $SharePointInfo = Get-SharePointAdminLink -Public $false -tenantFilter $TenantFilter - $sharepointQuota = (New-GraphGetRequest -scope "$($SharePointInfo.AdminUrl)/.default" -tenantid $TenantFilter -uri "$($SharePointInfo.AdminUrl)/_api/StorageQuotas()?api-version=1.3.2").value + $extraHeaders = @{ + 'Accept' = 'application/json' + } + $sharepointQuota = (New-GraphGetRequest -extraHeaders $extraHeaders -scope "$($SharePointInfo.AdminUrl)/.default" -tenantid $TenantFilter -uri "$($SharePointInfo.AdminUrl)/_api/StorageQuotas()?api-version=1.3.2") } catch { return }