diff --git a/CommunityRepos.json b/CommunityRepos.json index e34aafb8c821..9d6d702d0161 100644 --- a/CommunityRepos.json +++ b/CommunityRepos.json @@ -1,4 +1,22 @@ [ + { + "Id": "1041442982", + "Name": "CISTemplates", + "Description": "CIPP CIS Templates", + "URL": "https://github.com/CyberDrain/CyberDrain-CIS-Templates", + "FullName": "CyberDrain/CyberDrain-CIS-Templates", + "Owner": "CyberDrain", + "Visibility": "public", + "WriteAccess": false, + "DefaultBranch": "main", + "RepoPermissions": { + "admin": false, + "maintain": false, + "push": false, + "triage": false, + "pull": true + } + }, { "Id": "930523724", "Name": "CIPP-Templates", @@ -52,5 +70,23 @@ "triage": false, "pull": true } + }, + { + "Id": "863076113", + "Name": "IntuneBaseLines", + "Description": "In this repo, you will find Intune profiles in JSON format, which can be used in setting up your Modern Workplace. All policies were created in Microsoft Intune and exported to share with the community.", + "URL": "https://github.com/IntuneAdmin/IntuneBaselines", + "FullName": "IntuneAdmin/IntuneBaselines", + "Owner": "IntuneAdmin", + "Visibility": "public", + "WriteAccess": false, + "DefaultBranch": "main", + "RepoPermissions": { + "admin": false, + "maintain": false, + "push": false, + "triage": false, + "pull": true + } } ] diff --git a/Config/SchedulerRateLimits.json b/Config/SchedulerRateLimits.json new file mode 100644 index 000000000000..3d2c65716af0 --- /dev/null +++ b/Config/SchedulerRateLimits.json @@ -0,0 +1,10 @@ +[ + { + "Command": "Sync-CIPPExtensionData", + "MaxRequests": 50 + }, + { + "Command": "Push-CIPPExtensionData", + "MaxRequests": 30 + } +] \ No newline at end of file diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertLicensedUsersWithRoles.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertLicensedUsersWithRoles.ps1 new file mode 100644 index 000000000000..d97625653c8e --- /dev/null +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertLicensedUsersWithRoles.ps1 @@ -0,0 +1,36 @@ +function Get-CIPPAlertLicensedUsersWithRoles { + <# + .FUNCTIONALITY + Entrypoint + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $false)] + [Alias('input')] + $InputValue, + $TenantFilter + ) + + # Get all users with assigned licenses + $LicensedUsers = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users?`$top=999&`$select=userPrincipalName,assignedLicenses,displayName" -tenantid $TenantFilter | Where-Object { $_.assignedLicenses -and $_.assignedLicenses.Count -gt 0 } + if (-not $LicensedUsers -or $LicensedUsers.Count -eq 0) { + Write-Information "No licensed users found for tenant $TenantFilter" + return $true + } + # Get all directory roles with their members + $DirectoryRoles = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/directoryRoles?`$expand=members" -tenantid $TenantFilter + if (-not $DirectoryRoles -or $DirectoryRoles.Count -eq 0) { + Write-Information "No directory roles found for tenant $TenantFilter" + return + } + $UsersToAlertOn = $LicensedUsers | Where-Object { $_.userPrincipalName -in $DirectoryRoles.members.userPrincipalName } + + + if ($UsersToAlertOn.Count -gt 0) { + Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $UsersToAlertOn + } else { + Write-Information "No licensed users with roles found for tenant $TenantFilter" + } + + +} diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertQuotaUsed.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertQuotaUsed.ps1 index 1e61ffaa01b6..d4e8c651e490 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertQuotaUsed.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertQuotaUsed.ps1 @@ -17,7 +17,7 @@ function Get-CIPPAlertQuotaUsed { return } $OverQuota = $AlertData | ForEach-Object { - if ($_.StorageUsedInBytes -eq 0 -or $_.prohibitSendReceiveQuotaInBytes -eq 0) { return } + if ([string]::IsNullOrEmpty($_.StorageUsedInBytes) -or [string]::IsNullOrEmpty($_.prohibitSendReceiveQuotaInBytes) -or $_.StorageUsedInBytes -eq 0 -or $_.prohibitSendReceiveQuotaInBytes -eq 0) { return } try { $PercentLeft = [math]::round(($_.storageUsedInBytes / $_.prohibitSendReceiveQuotaInBytes) * 100) } catch { $PercentLeft = 100 } diff --git a/Modules/CIPPCore/Public/AuditLogs/Get-CippAuditLogSearches.ps1 b/Modules/CIPPCore/Public/AuditLogs/Get-CippAuditLogSearches.ps1 index 693011c25aba..9301bb28aaba 100644 --- a/Modules/CIPPCore/Public/AuditLogs/Get-CippAuditLogSearches.ps1 +++ b/Modules/CIPPCore/Public/AuditLogs/Get-CippAuditLogSearches.ps1 @@ -13,27 +13,31 @@ function Get-CippAuditLogSearches { [Parameter()] [switch]$ReadyToProcess ) - + $AuditLogSearchesTable = Get-CippTable -TableName 'AuditLogSearches' if ($ReadyToProcess.IsPresent) { - $AuditLogSearchesTable = Get-CippTable -TableName 'AuditLogSearches' $15MinutesAgo = (Get-Date).AddMinutes(-15).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') $1DayAgo = (Get-Date).AddDays(-1).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') - $PendingQueries = Get-CIPPAzDataTableEntity @AuditLogSearchesTable -Filter "Tenant eq '$TenantFilter' and (CippStatus eq 'Pending' or (CippStatus eq 'Processing' and Timestamp le datetime'$15MinutesAgo')) and Timestamp ge datetime'$1DayAgo'" | Sort-Object Timestamp + $PendingQueries = Get-CIPPAzDataTableEntity @AuditLogSearchesTable -Filter "PartitionKey eq 'Search' and Tenant eq '$TenantFilter' and (CippStatus eq 'Pending' or (CippStatus eq 'Processing' and Timestamp le datetime'$15MinutesAgo')) and Timestamp ge datetime'$1DayAgo'" | Sort-Object Timestamp + } else { + $7DaysAgo = (Get-Date).AddDays(-7).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') + $PendingQueries = Get-CIPPAzDataTableEntity @AuditLogSearchesTable -Filter "Tenant eq '$TenantFilter' and Timestamp ge datetime'$7DaysAgo'" + } - $BulkRequests = foreach ($PendingQuery in $PendingQueries) { - @{ - id = $PendingQuery.RowKey - url = 'security/auditLog/queries/' + $PendingQuery.RowKey - method = 'GET' - } + $BulkRequests = foreach ($PendingQuery in $PendingQueries) { + @{ + id = $PendingQuery.RowKey + url = 'security/auditLog/queries/' + $PendingQuery.RowKey + method = 'GET' } - if ($BulkRequests.Count -eq 0) { - return @() - } - $Queries = New-GraphBulkRequest -Requests @($BulkRequests) -AsApp $true -TenantId $TenantFilter | Select-Object -ExpandProperty body + } + if ($BulkRequests.Count -eq 0) { + return @() + } + $Queries = New-GraphBulkRequest -Requests @($BulkRequests) -AsApp $true -TenantId $TenantFilter | Select-Object -ExpandProperty body + + if ($ReadyToProcess.IsPresent) { $Queries = $Queries | Where-Object { $PendingQueries.RowKey -contains $_.id -and $_.status -eq 'succeeded' } - } else { - $Queries = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/security/auditLog/queries' -AsApp $true -tenantid $TenantFilter } + return $Queries } diff --git a/Modules/CIPPCore/Public/AuditLogs/New-CippAuditLogSearch.ps1 b/Modules/CIPPCore/Public/AuditLogs/New-CippAuditLogSearch.ps1 index 2cbbd76e9e8d..7b07fcfe8c0f 100644 --- a/Modules/CIPPCore/Public/AuditLogs/New-CippAuditLogSearch.ps1 +++ b/Modules/CIPPCore/Public/AuditLogs/New-CippAuditLogSearch.ps1 @@ -157,20 +157,26 @@ function New-CippAuditLogSearch { if ($PSCmdlet.ShouldProcess('Create a new audit log search for tenant ' + $TenantFilter)) { $Query = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/security/auditLog/queries' -body ($SearchParams | ConvertTo-Json -Compress) -tenantid $TenantFilter -AsApp $true + if ($ProcessLogs.IsPresent -and $Query.id) { - $Entity = [PSCustomObject]@{ - PartitionKey = [string]'Search' - RowKey = [string]$Query.id - Tenant = [string]$TenantFilter - DisplayName = [string]$DisplayName - StartTime = [datetime]$StartTime.ToUniversalTime() - EndTime = [datetime]$EndTime.ToUniversalTime() - Query = [string]($Query | ConvertTo-Json -Compress) - CippStatus = [string]'Pending' - } - $Table = Get-CIPPTable -TableName 'AuditLogSearches' - Add-CIPPAzDataTableEntity @Table -Entity $Entity -Force | Out-Null + $CippStatus = 'Pending' + } else { + $CippStatus = 'N/A' + } + + $Entity = [PSCustomObject]@{ + PartitionKey = [string]'Search' + RowKey = [string]$Query.id + Tenant = [string]$TenantFilter + DisplayName = [string]$DisplayName + StartTime = [datetime]$StartTime.ToUniversalTime() + EndTime = [datetime]$EndTime.ToUniversalTime() + Query = [string]($Query | ConvertTo-Json -Compress) + CippStatus = [string]$CippStatus } + $Table = Get-CIPPTable -TableName 'AuditLogSearches' + Add-CIPPAzDataTableEntity @Table -Entity $Entity -Force | Out-Null + return $Query } } diff --git a/Modules/CIPPCore/Public/Authentication/Get-CIPPRolePermissions.ps1 b/Modules/CIPPCore/Public/Authentication/Get-CIPPRolePermissions.ps1 index 8bac3674e677..4b89c560b759 100644 --- a/Modules/CIPPCore/Public/Authentication/Get-CIPPRolePermissions.ps1 +++ b/Modules/CIPPCore/Public/Authentication/Get-CIPPRolePermissions.ps1 @@ -20,11 +20,13 @@ function Get-CIPPRolePermissions { $Permissions = $Role.Permissions | ConvertFrom-Json $AllowedTenants = if ($Role.AllowedTenants) { $Role.AllowedTenants | ConvertFrom-Json } else { @() } $BlockedTenants = if ($Role.BlockedTenants) { $Role.BlockedTenants | ConvertFrom-Json } else { @() } + $BlockedEndpoints = if ($Role.BlockedEndpoints) { $Role.BlockedEndpoints | ConvertFrom-Json } else { @() } [PSCustomObject]@{ - Role = $Role.RowKey - Permissions = $Permissions.PSObject.Properties.Value - AllowedTenants = @($AllowedTenants) - BlockedTenants = @($BlockedTenants) + Role = $Role.RowKey + Permissions = $Permissions.PSObject.Properties.Value + AllowedTenants = @($AllowedTenants) + BlockedTenants = @($BlockedTenants) + BlockedEndpoints = @($BlockedEndpoints) } } else { throw "Role $RoleName not found." diff --git a/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 b/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 index 43b3de6e8141..71d78fae0ac5 100644 --- a/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 +++ b/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 @@ -199,6 +199,7 @@ function Test-CIPPAccess { continue } } + if ($PermissionsFound) { if ($TenantList.IsPresent) { $LimitedTenantList = foreach ($Permission in $PermissionSet) { @@ -248,6 +249,9 @@ function Test-CIPPAccess { foreach ($Role in $PermissionSet) { foreach ($Perm in $Role.Permissions) { if ($Perm -match $APIRole) { + if ($Role.BlockedEndpoints -contains $Request.Params.CIPPEndpoint) { + throw "Access to this CIPP API endpoint is not allowed, the custom role '$($Role.Role)' has blocked this endpoint: $($Request.Params.CIPPEndpoint)" + } $APIAllowed = $true break } diff --git a/Modules/CIPPCore/Public/CippQueue/Invoke-ListCippQueue.ps1 b/Modules/CIPPCore/Public/CippQueue/Invoke-ListCippQueue.ps1 index e9c33daf0ebf..34e9fbfa191a 100644 --- a/Modules/CIPPCore/Public/CippQueue/Invoke-ListCippQueue.ps1 +++ b/Modules/CIPPCore/Public/CippQueue/Invoke-ListCippQueue.ps1 @@ -5,20 +5,32 @@ function Invoke-ListCippQueue { .ROLE CIPP.Core.Read #> - param($Request = $null, $TriggerMetadata = $null) + param($Request = $null, $TriggerMetadata = $null, $Reference = $null, $QueueId = $null) if ($Request) { $APIName = $Request.Params.CIPPEndpoint Write-LogMessage -headers $Request.Headers -API $APINAME -message 'Accessed this API' -Sev 'Debug' } + $QueueId = $Request.Query.QueueId ?? $QueueId + $Reference = $Request.Query.Reference ?? $Reference + $CippQueue = Get-CippTable -TableName 'CippQueue' $CippQueueTasks = Get-CippTable -TableName 'CippQueueTasks' $3HoursAgo = (Get-Date).ToUniversalTime().AddHours(-3).ToString('yyyy-MM-ddTHH:mm:ssZ') - $CippQueueData = Get-CIPPAzDataTableEntity @CippQueue -Filter "PartitionKey eq 'CippQueue' and Timestamp ge datetime'$3HoursAgo'" | Sort-Object -Property Timestamp -Descending + + if ($QueueId) { + $Filter = "PartitionKey eq 'CippQueue' and RowKey eq '$QueueId'" + } elseif ($Reference) { + $Filter = "PartitionKey eq 'CippQueue' and Reference eq '$Reference' and Timestamp ge datetime'$3HoursAgo'" + } else { + $Filter = "PartitionKey eq 'CippQueue' and Timestamp ge datetime'$3HoursAgo'" + } + + $CippQueueData = Get-CIPPAzDataTableEntity @CippQueue -Filter $Filter | Sort-Object -Property Timestamp -Descending $QueueData = foreach ($Queue in $CippQueueData) { - $Tasks = Get-CIPPAzDataTableEntity @CippQueueTasks -Filter "PartitionKey eq 'Task' and QueueId eq '$($Queue.RowKey)'" | Where-Object { $_.Name } | Select-Object @{n = 'Timestamp'; exp = { $_.Timestamp.DateTime.ToUniversalTime() } }, Name, Status + $Tasks = Get-CIPPAzDataTableEntity @CippQueueTasks -Filter "PartitionKey eq 'Task' and QueueId eq '$($Queue.RowKey)'" | Where-Object { $_.Name } | Select-Object @{n = 'Timestamp'; exp = { $_.Timestamp } }, Name, Status $TaskStatus = @{} $Tasks | Group-Object -Property Status | ForEach-Object { $TaskStatus.$($_.Name) = $_.Count @@ -54,9 +66,9 @@ function Invoke-ListCippQueue { PercentComplete = [math]::Round(((($TotalCompleted + $TotalFailed) / $Queue.TotalTasks) * 100), 1) PercentFailed = [math]::Round((($TotalFailed / $Queue.TotalTasks) * 100), 1) PercentRunning = [math]::Round((($TotalRunning / $Queue.TotalTasks) * 100), 1) - Tasks = @($Tasks) + Tasks = @($Tasks | Sort-Object -Descending Timestamp) Status = $Queue.Status - Timestamp = $Queue.Timestamp.DateTime.ToUniversalTime() + Timestamp = $Queue.Timestamp } } diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/BEC/Push-BECRun.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/BEC/Push-BECRun.ps1 index b23079f413bb..9c1ffb9b5ab8 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/BEC/Push-BECRun.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/BEC/Push-BECRun.ps1 @@ -50,7 +50,7 @@ function Push-BECRun { $ExtractResult = 'Successfully extracted logs from auditlog' } Write-Information 'Getting last sign-in' - Try { + try { $URI = "https://graph.microsoft.com/beta/auditLogs/signIns?`$filter=(userId eq '$SuspectUser')&`$top=1&`$orderby=createdDateTime desc" $LastSignIn = New-GraphGetRequest -uri $URI -tenantid $TenantFilter -noPagination $true -verbose | Select-Object @{ Name = 'CreatedDateTime'; Expression = { $(($_.createdDateTime | Out-String) -replace '\r\n') } }, id, @@ -69,7 +69,7 @@ function Push-BECRun { #List all users devices $Bytes = [System.Text.Encoding]::UTF8.GetBytes($SuspectUser) $base64IdentityParam = [Convert]::ToBase64String($Bytes) - Try { + try { $Devices = New-GraphGetRequest -uri "https://outlook.office365.com:443/adminapi/beta/$($TenantFilter)/mailbox('$($base64IdentityParam)')/MobileDevice/Exchange.GetMobileDeviceStatistics()/?IsEncoded=True" -Tenantid $TenantFilter -scope ExchangeOnline } catch { $Devices = $null @@ -143,10 +143,10 @@ function Push-BECRun { Write-Information 'Getting bulk requests' $GraphResults = New-GraphBulkRequest -Requests $Requests -tenantid $TenantFilter -asapp $true - $PasswordChanges = ($GraphResults | Where-Object { $_.id -eq 'Users' }).body.value | Where-Object { $_.lastPasswordChangeDateTime -ge $startDate } - $NewUsers = ($GraphResults | Where-Object { $_.id -eq 'Users' }).body.value | Where-Object { $_.createdDateTime -ge $startDate } - $MFADevices = ($GraphResults | Where-Object { $_.id -eq 'MFADevices' }).body.value - $NewSPs = ($GraphResults | Where-Object { $_.id -eq 'NewSPs' }).body.value + $PasswordChanges = ($GraphResults | Where-Object { $_.id -eq 'Users' }).body.value | Where-Object { $_.lastPasswordChangeDateTime -ge $startDate } ?? @() + $NewUsers = ($GraphResults | Where-Object { $_.id -eq 'Users' }).body.value | Where-Object { $_.createdDateTime -ge $startDate } ?? @() + $MFADevices = ($GraphResults | Where-Object { $_.id -eq 'MFADevices' }).body.value ?? @() + $NewSPs = ($GraphResults | Where-Object { $_.id -eq 'NewSPs' }).body.value ?? @() $Results = [PSCustomObject]@{ diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Graph Requests/Push-ListGraphRequestQueue.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Graph Requests/Push-ListGraphRequestQueue.ps1 index dc7c33e94bd5..9c838711e42c 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Graph Requests/Push-ListGraphRequestQueue.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Graph Requests/Push-ListGraphRequestQueue.ps1 @@ -35,6 +35,7 @@ function Push-ListGraphRequestQueue { ReverseTenantLookupProperty = $Item.ReverseTenantLookupProperty ReverseTenantLookup = $Item.ReverseTenantLookup AsApp = $Item.AsApp ?? $false + Caller = 'Push-ListGraphRequestQueue' SkipCache = $true } diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecMdoAlertsListAllTenants.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecMdoAlertsListAllTenants.ps1 new file mode 100644 index 000000000000..15cab5352f97 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecMdoAlertsListAllTenants.ps1 @@ -0,0 +1,48 @@ +function Push-ExecMdoAlertsListAllTenants { + <# + .FUNCTIONALITY + Entrypoint + #> + param($Item) + + $Tenant = Get-Tenants -TenantFilter $Item.customerId + $domainName = $Tenant.defaultDomainName + $Table = Get-CIPPTable -TableName 'cachealertsandincidents' + + try { + # Get MDO alerts using the specific endpoint and filter + $Alerts = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/security/alerts_v2?`$filter=serviceSource eq 'microsoftDefenderForOffice365'" -tenantid $domainName + + foreach ($Alert in $Alerts) { + $GUID = (New-Guid).Guid + $GraphRequest = @{ + MdoAlert = [string]($Alert | ConvertTo-Json -Depth 10) + RowKey = [string]$GUID + PartitionKey = 'MdoAlert' + Tenant = [string]$domainName + } + Add-CIPPAzDataTableEntity @Table -Entity $GraphRequest -Force | Out-Null + } + + } catch { + $GUID = (New-Guid).Guid + $AlertText = ConvertTo-Json -InputObject @{ + Tenant = $domainName + displayName = "Could not connect to Tenant: $($_.Exception.Message)" + id = '' + severity = 'CIPP' + status = 'Failed' + createdDateTime = (Get-Date).ToString('s') + category = 'Unknown' + description = 'Could not connect' + serviceSource = 'microsoftDefenderForOffice365' + } + $GraphRequest = @{ + MdoAlert = [string]$AlertText + RowKey = [string]$GUID + PartitionKey = 'MdoAlert' + Tenant = [string]$domainName + } + Add-CIPPAzDataTableEntity @Table -Entity $GraphRequest -Force | Out-Null + } +} diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ListMailboxRulesQueue.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ListMailboxRulesQueue.ps1 index 1c826fbfa0cb..b26603bd13e0 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ListMailboxRulesQueue.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ListMailboxRulesQueue.ps1 @@ -12,14 +12,20 @@ function Push-ListMailboxRulesQueue { $Table = Get-CIPPTable -TableName cachembxrules try { - $Rules = New-ExoRequest -tenantid $domainName -cmdlet 'Get-Mailbox' -Select 'userPrincipalName,GUID' | ForEach-Object -Parallel { - Import-Module CIPPCore - $MbxRules = New-ExoRequest -Anchor $_.UserPrincipalName -tenantid $using:domainName -cmdlet 'Get-InboxRule' -cmdParams @{Mailbox = $_.GUID; IncludeHidden = $true } | Where-Object { $_.Name -ne 'Junk E-Mail Rule' -and $_.Name -notlike 'Microsoft.Exchange.OOF.*' } - foreach ($Rule in $MbxRules) { - $Rule | Add-Member -NotePropertyName 'UserPrincipalName' -NotePropertyValue $_.userPrincipalName - $Rule + $Mailboxes = New-ExoRequest -tenantid $domainName -cmdlet 'Get-Mailbox' -Select 'userPrincipalName,GUID' + $Request = $Mailboxes | ForEach-Object { + @{ + OperationGuid = $_.UserPrincipalName + CmdletInput = @{ + CmdletName = 'Get-InboxRule' + Parameters = @{ + Mailbox = $_.UserPrincipalName + } + } } } + + $Rules = New-ExoBulkRequest -tenantid $domainName -cmdletArray @($Request) | Where-Object { $_.Identity } if (($Rules | Measure-Object).Count -gt 0) { $GraphRequest = foreach ($Rule in $Rules) { [PSCustomObject]@{ diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-UpdateTenants.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-UpdateTenants.ps1 index d3ef1e2711c8..d4674c69038b 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-UpdateTenants.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-UpdateTenants.ps1 @@ -5,7 +5,7 @@ function Push-UpdateTenants { #> Param($Item) $QueueReference = 'UpdateTenants' - $RunningQueue = Invoke-ListCippQueue | Where-Object { $_.Reference -eq $QueueReference -and $_.Status -ne 'Completed' -and $_.Status -ne 'Failed' } + $RunningQueue = Invoke-ListCippQueue -Reference $QueueReference | Where-Object { $_.Status -ne 'Completed' -and $_.Status -ne 'Failed' } $Queue = New-CippQueueEntry -Name 'Update Tenants' -Reference $QueueReference -TotalTasks 1 try { diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecSetPackageTag.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecSetPackageTag.ps1 new file mode 100644 index 000000000000..c55f0fbeef68 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecSetPackageTag.ps1 @@ -0,0 +1,48 @@ +using namespace System.Net + +function Invoke-ExecSetPackageTag { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + CIPP.Core.ReadWrite + #> + [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' + + try { + $GUIDS = $Request.body.GUID + $PackageName = $Request.body.Package | Select-Object -First 1 + foreach ($GUID in $GUIDS) { + $Filter = "RowKey eq '$GUID'" + $Template = Get-CIPPAzDataTableEntity @Table -Filter $Filter + Add-CIPPAzDataTableEntity @Table -Entity @{ + JSON = $Template.JSON + RowKey = "$GUID" + PartitionKey = $Template.PartitionKey + GUID = "$GUID" + Package = "$PackageName" + } -Force + + Write-LogMessage -headers $Headers -API $APIName -message "Successfully updated template with GUID $GUID with package tag: $PackageName" -Sev 'Info' + } + + $body = [pscustomobject]@{ 'Results' = "Successfully updated template(s) with package tag: $PackageName" } + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -headers $Headers -API $APIName -message "Failed to set package tag: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage + $body = [pscustomobject]@{'Results' = "Failed to set package tag: $($ErrorMessage.NormalizedError)" } + } + + # 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/CIPP/Core/Invoke-ListGraphRequest.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListGraphRequest.ps1 index 54ad46f0d529..9b7c58ae4b68 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListGraphRequest.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListGraphRequest.ps1 @@ -81,7 +81,7 @@ function Invoke-ListGraphRequest { } if ($Request.Query.manualPagination) { - $GraphRequestParams.NoPagination = [System.Boolean]$Request.Query.manualPagination + $GraphRequestParams.ManualPagination = [System.Boolean]$Request.Query.manualPagination } if ($Request.Query.nextLink) { @@ -139,7 +139,7 @@ function Invoke-ListGraphRequest { if ($Results.Queued -eq $true) { $Metadata.Queued = $Results.Queued $Metadata.QueueMessage = $Results.QueueMessage - $Metadata.QueuedId = $Results.QueueId + $Metadata.QueueId = $Results.QueueId $Results = @() } } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecAddTrustedIP.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecAddTrustedIP.ps1 index 41a0ae5a2854..8edf5c9f2681 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecAddTrustedIP.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecAddTrustedIP.ps1 @@ -1,6 +1,6 @@ using namespace System.Net -Function Invoke-ExecAddTrustedIP { +function Invoke-ExecAddTrustedIP { <# .FUNCTIONALITY Entrypoint @@ -11,12 +11,13 @@ Function Invoke-ExecAddTrustedIP { param($Request, $TriggerMetadata) $Table = Get-CippTable -tablename 'trustedIps' - Add-CIPPAzDataTableEntity @Table -Entity @{ - PartitionKey = $Request.Body.tenantfilter - RowKey = $Request.Body.IP - state = $Request.Body.State - } -Force - + foreach ($IP in $Request.body.IP) { + Add-CIPPAzDataTableEntity @Table -Entity @{ + PartitionKey = $Request.Body.tenantfilter + RowKey = $IP + state = $Request.Body.State + } -Force + } Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::OK Body = @{ results = "Added $($Request.Body.IP) to database with state $($Request.Body.State) for $($Request.Body.tenantfilter)" } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCustomRole.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCustomRole.ps1 index 738aacfd7183..e95de28bbc65 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCustomRole.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCustomRole.ps1 @@ -26,11 +26,12 @@ function Invoke-ExecCustomRole { Write-LogMessage -headers $Request.Headers -API 'ExecCustomRole' -message "Saved custom role $($Request.Body.RoleName)" -Sev 'Info' if ($Request.Body.RoleName -notin $DefaultRoles) { $Role = @{ - 'PartitionKey' = 'CustomRoles' - 'RowKey' = "$($Request.Body.RoleName.ToLower())" - 'Permissions' = "$($Request.Body.Permissions | ConvertTo-Json -Compress)" - 'AllowedTenants' = "$($Request.Body.AllowedTenants | ConvertTo-Json -Compress)" - 'BlockedTenants' = "$($Request.Body.BlockedTenants | ConvertTo-Json -Compress)" + 'PartitionKey' = 'CustomRoles' + 'RowKey' = "$($Request.Body.RoleName.ToLower())" + 'Permissions' = "$($Request.Body.Permissions | ConvertTo-Json -Compress)" + 'AllowedTenants' = "$($Request.Body.AllowedTenants | ConvertTo-Json -Compress)" + 'BlockedTenants' = "$($Request.Body.BlockedTenants | ConvertTo-Json -Compress)" + 'BlockedEndpoints' = "$($Request.Body.BlockedEndpoints | ConvertTo-Json -Compress)" } Add-CIPPAzDataTableEntity @Table -Entity $Role -Force | Out-Null $Results.Add("Custom role $($Request.Body.RoleName) saved") @@ -110,6 +111,15 @@ function Invoke-ExecCustomRole { } else { $Role | Add-Member -NotePropertyName BlockedTenants -NotePropertyValue @() -Force } + if ($Role.BlockedEndpoints) { + try { + $Role.BlockedEndpoints = @($Role.BlockedEndpoints | ConvertFrom-Json) + } catch { + $Role.BlockedEndpoints = '' + } + } else { + $Role | Add-Member -NotePropertyName BlockedEndpoints -NotePropertyValue @() -Force + } $EntraRoleGroup = $EntraRoleGroups | Where-Object -Property RowKey -EQ $Role.RowKey if ($EntraRoleGroup) { $EntraGroup = $EntraRoleGroups | Where-Object -Property RowKey -EQ $Role.RowKey | Select-Object @{Name = 'label'; Expression = { $_.GroupName } }, @{Name = 'value'; Expression = { $_.GroupId } } @@ -120,10 +130,11 @@ function Invoke-ExecCustomRole { } $DefaultRoles = foreach ($DefaultRole in $DefaultRoles) { $Role = @{ - RowKey = $DefaultRole - Permissions = '' - AllowedTenants = @('AllTenants') - BlockedTenants = @('') + RowKey = $DefaultRole + Permissions = '' + AllowedTenants = @('AllTenants') + BlockedTenants = @('') + BlockedEndpoints = @('') } $EntraRoleGroup = $EntraRoleGroups | Where-Object -Property RowKey -EQ $Role.RowKey if ($EntraRoleGroup) { diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecHVEUser.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecHVEUser.ps1 new file mode 100644 index 000000000000..fca702d1c2e8 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecHVEUser.ps1 @@ -0,0 +1,108 @@ +using namespace System.Net + +function Invoke-ExecHVEUser { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Exchange.Mailbox.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -Headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + + $Results = [System.Collections.Generic.List[string]]::new() + $HVEUserObject = $Request.Body + $Tenant = $HVEUserObject.TenantFilter + + try { + # Check if Security Defaults are enabled + try { + $SecurityDefaults = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/identitySecurityDefaultsEnforcementPolicy' -tenantid $Tenant + if ($SecurityDefaults.isEnabled -eq $true) { + $Results.Add('WARNING: Security Defaults are enabled for this tenant. HVE might not function.') + } + } catch { + $Results.Add('WARNING: Could not check Security Defaults status. Please verify authentication policies manually.') + } + + # Create the HVE user using New-MailUser + $BodyToShip = [pscustomobject] @{ + Name = $HVEUserObject.displayName + DisplayName = $HVEUserObject.displayName + PrimarySmtpAddress = $HVEUserObject.primarySMTPAddress + Password = $HVEUserObject.password + HVEAccount = $true + } + + $CreateHVERequest = New-ExoRequest -tenantid $Tenant -cmdlet 'New-MailUser' -cmdParams $BodyToShip + $Results.Add("Successfully created HVE user: $($HVEUserObject.primarySMTPAddress)") + Write-LogMessage -Headers $Headers -API $APIName -tenant $Tenant -message "Created HVE user $($HVEUserObject.displayName) with email $($HVEUserObject.primarySMTPAddress)" -Sev 'Info' + + # Try to exclude from Conditional Access policies that block basic authentication + try { + # Get all Conditional Access policies + $CAPolicies = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/policies' -tenantid $Tenant + + $BasicAuthPolicies = $CAPolicies | Where-Object { + $_.conditions.clientAppTypes -contains 'exchangeActiveSync' -or + $_.conditions.clientAppTypes -contains 'other' -or + $_.conditions.applications.includeApplications -contains 'All' -and + $_.grantControls.builtInControls -contains 'block' + } + + if ($BasicAuthPolicies) { + foreach ($Policy in $BasicAuthPolicies) { + try { + # Add the HVE user to the exclusions + $ExcludedUsers = @($Policy.conditions.users.excludeUsers) + if ($CreateHVERequest.ExternalDirectoryObjectId -notin $ExcludedUsers) { + + $ExcludeUsers = @($ExcludedUsers + $CreateHVERequest.ExternalDirectoryObjectId) + $UpdateBody = @{ + conditions = @{ + users = @{ + excludeUsers = @($ExcludeUsers | Sort-Object -Unique) + } + } + } + + $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/identity/conditionalAccess/policies/$($Policy.id)" -type PATCH -body (ConvertTo-Json -InputObject $UpdateBody -Depth 10) -tenantid $Tenant + $Results.Add("Excluded HVE user from Conditional Access policy: $($Policy.displayName)") + Write-LogMessage -Headers $Headers -API $APIName -tenant $Tenant -message "Excluded HVE user from CA policy: $($Policy.displayName)" -Sev 'Info' + } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Message = "Failed to exclude from CA policy '$($Policy.displayName)': $($ErrorMessage.NormalizedError)" + Write-LogMessage -Headers $Headers -API $APIName -tenant $Tenant -message $Message -Sev 'Warning' -LogData $ErrorMessage + $Results.Add($Message) + } + } + } else { + $Results.Add('No Conditional Access policies blocking basic authentication found.') + } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Message = "Failed to check/update Conditional Access policies: $($ErrorMessage.NormalizedError)" + Write-LogMessage -Headers $Headers -API $APIName -tenant $Tenant -message $Message -Sev 'Warning' -LogData $ErrorMessage + $Results.Add($Message) + } + + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Message = "Failed to create HVE user: $($ErrorMessage.NormalizedError)" + Write-LogMessage -Headers $Headers -API $APIName -tenant $Tenant -message $Message -Sev 'Error' -LogData $ErrorMessage + $Results.Add($Message) + $StatusCode = [HttpStatusCode]::Forbidden + } + + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{ Results = @($Results) } + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecModifyContactPerms.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecModifyContactPerms.ps1 new file mode 100644 index 000000000000..894765475b17 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecModifyContactPerms.ps1 @@ -0,0 +1,116 @@ +using namespace System.Net + +function Invoke-ExecModifyContactPerms { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Exchange.Mailbox.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + + $Username = $Request.Body.userID + $TenantFilter = $Request.Body.tenantFilter + $Permissions = $Request.Body.permissions + + 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 + Body = $Body + }) + return + } + + try { + $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 + Body = $Body + }) + return + } + + $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 { + $Permissions = @($Permissions) + } + } + + Write-LogMessage -headers $Headers -API $APIName -message "Processing $($Permissions.Count) permission entries" -Sev 'Debug' + + foreach ($Permission in $Permissions) { + 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 + $FolderName = $Permission.FolderName ?? 'Contact' + $SendNotificationToUser = $Permission.SendNotificationToUser ?? $false + + 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 $Headers -API $APIName -message "Target Users: $($TargetUsers -join ', ')" -Sev 'Debug' + + foreach ($TargetUser in $TargetUsers) { + try { + Write-LogMessage -headers $Headers -API $APIName -message "Processing target user: $TargetUser" -Sev 'Debug' + $Params = @{ + APIName = $APIName + Headers = $Headers + RemoveAccess = if ($Modification -eq 'Remove') { $TargetUser } else { $null } + TenantFilter = $TenantFilter + UserID = $UserId + folderName = $FolderName + UserToGetPermissions = $TargetUser + LoggingName = $TargetUser + Permissions = $PermissionLevel + SendNotificationToUser = $SendNotificationToUser + } + + # Write-Host "Request params: $($Params | ConvertTo-Json)" + $Result = Set-CIPPContactPermission @Params + + $null = $Results.Add($Result) + } catch { + $HasErrors = $true + $null = $Results.Add("$($_.Exception.Message)") + } + } + } + + 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) } + + # 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 + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListContactPermissions.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListContactPermissions.ps1 new file mode 100644 index 000000000000..32162c24e3f8 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListContactPermissions.ps1 @@ -0,0 +1,41 @@ +using namespace System.Net + +Function Invoke-ListContactPermissions { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Exchange.Mailbox.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + + $UserID = $Request.Query.UserID + $TenantFilter = $Request.Query.tenantFilter + + try { + $GetContactParam = @{Identity = $UserID; FolderScope = 'Contacts' } + $ContactFolder = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-MailboxFolderStatistics' -anchor $UserID -cmdParams $GetContactParam | Select-Object -First 1 -ExcludeProperty *data.type* + $ContactParam = @{Identity = "$($UserID):\$($ContactFolder.name)" } + $Mailbox = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-Mailbox' -cmdParams @{Identity = $UserID } + $GraphRequest = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-MailboxFolderPermission' -anchor $UserID -cmdParams $ContactParam -UseSystemMailbox $true | Select-Object Identity, User, AccessRights, FolderName, @{ Name = 'MailboxInfo'; Expression = { $Mailbox } } + + Write-LogMessage -API $APIName -tenant $TenantFilter -message "Contact permissions listed for $($TenantFilter)" -sev Debug + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + $StatusCode = [HttpStatusCode]::Forbidden + $GraphRequest = $ErrorMessage + } + + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @($GraphRequest) + }) + +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxRules.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxRules.ps1 index 0c0b1c954322..165a06a82e21 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxRules.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxRules.ps1 @@ -1,6 +1,6 @@ using namespace System.Net -Function Invoke-ListMailboxRules { +function Invoke-ListMailboxRules { <# .FUNCTIONALITY Entrypoint @@ -19,28 +19,30 @@ Function Invoke-ListMailboxRules { $Table = Get-CIPPTable -TableName cachembxrules if ($TenantFilter -ne 'AllTenants') { - $Table.Filter = "Tenant eq '$TenantFilter'" + $Table.Filter = "PartitionKey eq 'MailboxRules' and Tenant eq '$TenantFilter'" + } else { + $Table.Filter = "PartitionKey eq 'MailboxRules'" } + + Write-Information 'Getting cached mailbox rules' $Rows = Get-CIPPAzDataTableEntity @Table | Where-Object -Property Timestamp -GT (Get-Date).AddHours(-1) $PartitionKey = 'MailboxRules' $QueueReference = '{0}-{1}' -f $TenantFilter, $PartitionKey - $RunningQueue = Invoke-ListCippQueue | Where-Object { $_.Reference -eq $QueueReference -and $_.Status -notmatch 'Completed' -and $_.Status -notmatch 'Failed' } + $RunningQueue = Invoke-ListCippQueue -Reference $QueueReference | Where-Object { $_.Status -notmatch 'Completed' -and $_.Status -notmatch 'Failed' } $Metadata = @{} # If a queue is running, we will not start a new one - if ($RunningQueue) { + if ($RunningQueue -and !$Rows) { + Write-Information "Queue is already running for $TenantFilter" $Metadata = [PSCustomObject]@{ QueueMessage = "Still loading data for $TenantFilter. Please check back in a few more minutes" + QueueId = $RunningQueue.RowKey } [PSCustomObject]@{ Waiting = $true } } elseif ((!$Rows -and !$RunningQueue) -or ($TenantFilter -eq 'AllTenants' -and ($Rows | Measure-Object).Count -eq 1)) { - # If no rows are found and no queue is running, we will start a new one - $Metadata = [PSCustomObject]@{ - QueueMessage = "Loading data for $TenantFilter. Please check back in 1 minute" - } - + Write-Information "No cached mailbox rules found for $TenantFilter, starting new orchestration" if ($TenantFilter -eq 'AllTenants') { $Tenants = Get-Tenants -IncludeErrors | Select-Object defaultDomainName $Type = 'All Tenants' @@ -49,6 +51,12 @@ Function Invoke-ListMailboxRules { $Type = $TenantFilter } $Queue = New-CippQueueEntry -Name "Mailbox Rules ($Type)" -Reference $QueueReference -TotalTasks ($Tenants | Measure-Object).Count + # If no rows are found and no queue is running, we will start a new one + $Metadata = [PSCustomObject]@{ + QueueMessage = "Loading data for $TenantFilter. Please check back in 1 minute" + QueueId = $Queue.RowKey + } + $Batch = $Tenants | Select-Object defaultDomainName, @{Name = 'FunctionName'; Expression = { 'ListMailboxRulesQueue' } }, @{Name = 'QueueName'; Expression = { $_.defaultDomainName } }, @{Name = 'QueueId'; Expression = { $Queue.RowKey } } if (($Batch | Measure-Object).Count -gt 0) { $InputObject = [PSCustomObject]@{ @@ -62,9 +70,8 @@ Function Invoke-ListMailboxRules { } } else { - if ($TenantFilter -ne 'AllTenants') { - $Rows = $Rows | Where-Object -Property Tenant -EQ $TenantFilter - $Rows = $Rows + $Metadata = [PSCustomObject]@{ + QueueId = $RunningQueue.RowKey ?? $null } $GraphRequest = $Rows | ForEach-Object { $NewObj = $_.Rules | ConvertFrom-Json -ErrorAction SilentlyContinue diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListMailQuarantine.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListMailQuarantine.ps1 index a3d2f2629e95..a403fa79168a 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListMailQuarantine.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListMailQuarantine.ps1 @@ -29,6 +29,7 @@ function Invoke-ListMailQuarantine { if ($RunningQueue) { $Metadata = [PSCustomObject]@{ QueueMessage = 'Still loading data for all tenants. Please check back in a few more minutes' + QueueId = $RunningQueue.RowKey } [PSCustomObject]@{ Waiting = $true @@ -39,6 +40,7 @@ function Invoke-ListMailQuarantine { $Queue = New-CippQueueEntry -Name 'Mail Quarantine - All Tenants' -Reference $QueueReference -TotalTasks ($TenantList | Measure-Object).Count $Metadata = [PSCustomObject]@{ QueueMessage = 'Loading data for all tenants. Please check back in a few minutes' + QueueId = $Queue.RowKey } $InputObject = [PSCustomObject]@{ OrchestratorName = 'MailQuarantineOrchestrator' @@ -57,6 +59,9 @@ function Invoke-ListMailQuarantine { Waiting = $true } } else { + $Metadata = [PSCustomObject]@{ + QueueId = $RunningQueue.RowKey ?? $null + } $messages = $Rows foreach ($message in $messages) { $messageObj = $message.QuarantineMessage | ConvertFrom-Json diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Transport/Invoke-ListTransportRules.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Transport/Invoke-ListTransportRules.ps1 index 3f4b5c65e019..f9166f317897 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Transport/Invoke-ListTransportRules.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Transport/Invoke-ListTransportRules.ps1 @@ -1,6 +1,6 @@ using namespace System.Net -Function Invoke-ListTransportRules { +function Invoke-ListTransportRules { <# .FUNCTIONALITY Entrypoint @@ -28,12 +28,13 @@ Function Invoke-ListTransportRules { $Filter = "PartitionKey eq '$PartitionKey'" $Rows = Get-CIPPAzDataTableEntity @Table -filter $Filter | Where-Object -Property Timestamp -GT (Get-Date).AddMinutes(-60) $QueueReference = '{0}-{1}' -f $TenantFilter, $PartitionKey - $RunningQueue = Invoke-ListCippQueue | Where-Object { $_.Reference -eq $QueueReference -and $_.Status -notmatch 'Completed' -and $_.Status -notmatch 'Failed' } + $RunningQueue = Invoke-ListCippQueue -Reference $QueueReference | Where-Object { $_.Status -notmatch 'Completed' -and $_.Status -notmatch 'Failed' } # If a queue is running, we will not start a new one if ($RunningQueue) { $Metadata = [PSCustomObject]@{ QueueMessage = 'Still loading transport rules for all tenants. Please check back in a few more minutes' + QueueId = $RunningQueue.RowKey } } elseif (!$Rows -and !$RunningQueue) { # If no rows are found and no queue is running, we will start a new one @@ -41,6 +42,7 @@ Function Invoke-ListTransportRules { $Queue = New-CippQueueEntry -Name 'Transport Rules - All Tenants' -Link '/email/transport/list-rules?tenantFilter=AllTenants' -Reference $QueueReference -TotalTasks ($TenantList | Measure-Object).Count $Metadata = [PSCustomObject]@{ QueueMessage = 'Loading transport rules for all tenants. Please check back in a few minutes' + QueueId = $Queue.RowKey } $InputObject = [PSCustomObject]@{ OrchestratorName = 'TransportRuleOrchestrator' @@ -57,6 +59,9 @@ Function Invoke-ListTransportRules { Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) | Out-Null } else { # Return cached data + $Metadata = [PSCustomObject]@{ + QueueId = $RunningQueue.RowKey ?? $null + } $Rules = $Rows foreach ($rule in $Rules) { $RuleObj = $rule.TransportRule | ConvertFrom-Json diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-ExecSyncVPP.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-ExecSyncVPP.ps1 new file mode 100644 index 000000000000..d1bf225814cf --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-ExecSyncVPP.ps1 @@ -0,0 +1,47 @@ +using namespace System.Net + +function Invoke-ExecSyncVPP { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Endpoint.Application.ReadWrite + #> + [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.Body.tenantFilter ?? $Request.Query.tenantFilter + try { + # Get all VPP tokens and sync them + $VppTokens = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/deviceAppManagement/vppTokens' -tenantid $TenantFilter | Where-Object { $_.state -eq 'valid' } + + if ($null -eq $VppTokens -or $VppTokens.Count -eq 0) { + $Result = 'No VPP tokens found' + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Info + } else { + $SyncCount = 0 + foreach ($Token in $VppTokens) { + $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceAppManagement/vppTokens/$($Token.id)/syncLicenses" -tenantid $TenantFilter + $SyncCount++ + } + $Result = "Successfully started VPP sync for $SyncCount tokens" + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Info + } + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = 'Failed to start VPP sync' + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -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/Endpoint/Autopilot/Invoke-AddAutopilotConfig.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Autopilot/Invoke-AddAutopilotConfig.ps1 index 5112dc52d091..a95125b9eeaa 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Autopilot/Invoke-AddAutopilotConfig.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Autopilot/Invoke-AddAutopilotConfig.ps1 @@ -1,6 +1,6 @@ using namespace System.Net -Function Invoke-AddAutopilotConfig { +function Invoke-AddAutopilotConfig { <# .FUNCTIONALITY Entrypoint @@ -14,42 +14,34 @@ Function Invoke-AddAutopilotConfig { $Headers = $Request.Headers Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' - - - # Input bindings are passed in via param block. - $Tenants = $Request.body.selectedTenants.value - $AssignTo = if ($request.body.Assignto -ne 'on') { $request.body.Assignto } - $Profbod = [pscustomobject]$Request.body - $usertype = if ($Profbod.NotLocalAdmin -eq 'true') { 'standard' } else { 'administrator' } - $DeploymentMode = if ($profbod.DeploymentMode -eq 'true') { 'shared' } else { 'singleUser' } + $Tenants = $Request.Body.selectedTenants.value + $Profbod = [pscustomobject]$Request.Body + $UserType = if ($Profbod.NotLocalAdmin -eq 'true') { 'standard' } else { 'administrator' } + $DeploymentMode = if ($Profbod.DeploymentMode -eq 'true') { 'shared' } else { 'singleUser' } $profileParams = @{ - displayname = $request.body.Displayname - description = $request.body.Description - usertype = $usertype + DisplayName = $Request.Body.DisplayName + Description = $Request.Body.Description + UserType = $UserType DeploymentMode = $DeploymentMode - assignto = $AssignTo - devicenameTemplate = $Profbod.deviceNameTemplate - allowWhiteGlove = $Profbod.allowWhiteGlove - CollectHash = $Profbod.collectHash - hideChangeAccount = $Profbod.hideChangeAccount - hidePrivacy = $Profbod.hidePrivacy - hideTerms = $Profbod.hideTerms + AssignTo = $Request.Body.Assignto + DeviceNameTemplate = $Profbod.DeviceNameTemplate + AllowWhiteGlove = $Profbod.allowWhiteGlove + CollectHash = $Profbod.CollectHash + HideChangeAccount = $Profbod.HideChangeAccount + HidePrivacy = $Profbod.HidePrivacy + HideTerms = $Profbod.HideTerms Autokeyboard = $Profbod.Autokeyboard Language = $ProfBod.languages.value } - $results = foreach ($Tenant in $tenants) { - $profileParams['tenantFilter'] = $Tenant + $Results = foreach ($tenant in $Tenants) { + $profileParams['tenantFilter'] = $tenant Set-CIPPDefaultAPDeploymentProfile @profileParams } - $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/Endpoint/Autopilot/Invoke-AddEnrollment.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Autopilot/Invoke-AddEnrollment.ps1 index b8f8a4c34fe2..a21767f47bae 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Autopilot/Invoke-AddEnrollment.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Autopilot/Invoke-AddEnrollment.ps1 @@ -1,6 +1,6 @@ using namespace System.Net -Function Invoke-AddEnrollment { +function Invoke-AddEnrollment { <# .FUNCTIONALITY Entrypoint @@ -14,22 +14,29 @@ Function Invoke-AddEnrollment { $Headers = $Request.Headers Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' - - - # Input bindings are passed in via param block. - $Tenants = $Request.body.selectedTenants.value - $Profbod = $Request.body - $results = foreach ($Tenant in $tenants) { - Set-CIPPDefaultAPEnrollment -TenantFilter $Tenant -ShowProgress $Profbod.ShowProgress -BlockDevice $Profbod.blockDevice -AllowReset $Profbod.AllowReset -EnableLog $Profbod.EnableLog -ErrorMessage $Profbod.ErrorMessage -TimeOutInMinutes $Profbod.TimeOutInMinutes -AllowFail $Profbod.AllowFail -OBEEOnly $Profbod.OBEEOnly + $Tenants = $Request.Body.selectedTenants.value + $Profbod = $Request.Body + $Results = foreach ($Tenant in $Tenants) { + $ParamSplat = @{ + TenantFilter = $Tenant + ShowProgress = $Profbod.ShowProgress + BlockDevice = $Profbod.blockDevice + AllowReset = $Profbod.AllowReset + EnableLog = $Profbod.EnableLog + ErrorMessage = $Profbod.ErrorMessage + TimeOutInMinutes = $Profbod.TimeOutInMinutes + AllowFail = $Profbod.AllowFail + OBEEOnly = $Profbod.OBEEOnly + InstallWindowsUpdates = $Profbod.InstallWindowsUpdates + } + Set-CIPPDefaultAPEnrollment @ParamSplat } - $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/Endpoint/Autopilot/Invoke-ListAutopilotconfig.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Autopilot/Invoke-ListAutopilotconfig.ps1 index a9a7bfdc0717..f79ed07ab422 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Autopilot/Invoke-ListAutopilotconfig.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Autopilot/Invoke-ListAutopilotconfig.ps1 @@ -1,6 +1,6 @@ using namespace System.Net -Function Invoke-ListAutopilotconfig { +function Invoke-ListAutopilotconfig { <# .FUNCTIONALITY Entrypoint @@ -15,18 +15,16 @@ Function Invoke-ListAutopilotconfig { 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 { - if ($request.query.type -eq 'ApProfile') { + if ($Request.Query.type -eq 'ApProfile') { $GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles?`$expand=assignments" -tenantid $TenantFilter } - if ($request.query.type -eq 'ESP') { - $GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/deviceEnrollmentConfigurations?`$expand=assignments" -tenantid $TenantFilter | Where-Object -Property '@odata.type' -EQ '#microsoft.graph.windows10EnrollmentCompletionPageConfiguration' + if ($Request.Query.type -eq 'ESP') { + $GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/deviceEnrollmentConfigurations?`$expand=assignments" -tenantid $TenantFilter | + Where-Object -Property '@odata.type' -EQ '#microsoft.graph.windows10EnrollmentCompletionPageConfiguration' } $StatusCode = [HttpStatusCode]::OK } catch { diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Autopilot/Invoke-RemoveAutopilotConfig.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Autopilot/Invoke-RemoveAutopilotConfig.ps1 new file mode 100644 index 000000000000..ba09de16a199 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Autopilot/Invoke-RemoveAutopilotConfig.ps1 @@ -0,0 +1,56 @@ +using namespace System.Net + +function Invoke-RemoveAutopilotConfig { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Endpoint.Autopilot.ReadWrite + #> + [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.Body.tenantFilter + $ProfileId = $Request.Body.ID + $DisplayName = $Request.Body.displayName + $Assignments = $Request.Body.assignments + + try { + # Validate required parameters + if ([string]::IsNullOrEmpty($ProfileId)) { + throw 'Profile ID is required' + } + + if ([string]::IsNullOrEmpty($TenantFilter)) { + throw 'Tenant filter is required' + } + + # Call the helper function to delete the autopilot profile + $params = @{ + ProfileId = $ProfileId + DisplayName = $DisplayName + TenantFilter = $TenantFilter + Assignments = $Assignments + Headers = $Headers + APIName = $APIName + } + $Result = Remove-CIPPAutopilotProfile @params + $StatusCode = [HttpStatusCode]::OK + + } catch { + $ErrorMessage = $_.Exception.Message + $Result = $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-ExecGetRecoveryKey.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ExecGetRecoveryKey.ps1 index 2ab268e9a852..e62aa8b30293 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ExecGetRecoveryKey.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ExecGetRecoveryKey.ps1 @@ -1,6 +1,6 @@ using namespace System.Net -Function Invoke-ExecGetRecoveryKey { +function Invoke-ExecGetRecoveryKey { <# .FUNCTIONALITY Entrypoint @@ -19,7 +19,7 @@ Function Invoke-ExecGetRecoveryKey { $GUID = $Request.Query.GUID ?? $Request.Body.GUID try { - $Result = Get-CIPPBitLockerKey -device $GUID -tenantFilter $TenantFilter -APIName $APIName -Headers $Headers + $Result = Get-CIPPBitLockerKey -Device $GUID -TenantFilter $TenantFilter -APIName $APIName -Headers $Headers $StatusCode = [HttpStatusCode]::OK } catch { $Result = $_.Exception.Message diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneTemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneTemplates.ps1 index 518bc40a7ac8..06afc34a1ca1 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneTemplates.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneTemplates.ps1 @@ -1,6 +1,6 @@ using namespace System.Net -Function Invoke-ListIntuneTemplates { +function Invoke-ListIntuneTemplates { <# .FUNCTIONALITY Entrypoint,AnyTenant @@ -45,6 +45,7 @@ Function Invoke-ListIntuneTemplates { $data | Add-Member -NotePropertyName 'description' -NotePropertyValue $JSONData.Description -Force $data | Add-Member -NotePropertyName 'Type' -NotePropertyValue $JSONData.Type -Force $data | Add-Member -NotePropertyName 'GUID' -NotePropertyValue $_.RowKey -Force + $data | Add-Member -NotePropertyName 'package' -NotePropertyValue $_.Package -Force $data } catch { @@ -52,7 +53,35 @@ Function Invoke-ListIntuneTemplates { } | Sort-Object -Property displayName } else { - $Templates = $RawTemplates.JSON | ForEach-Object { try { ConvertFrom-Json -InputObject $_ -Depth 100 -ErrorAction SilentlyContinue } catch {} } + if ($Request.query.mode -eq 'Tag') { + #when the mode is tag, show all the potential tags, return the object with: label: tag, value: tag, count: number of templates with that tag, unique only + $Templates = $RawTemplates | Where-Object { $_.Package } | Select-Object -Property Package | ForEach-Object { + $package = $_.Package + [pscustomobject]@{ + label = "$($package) ($(($RawTemplates | Where-Object { $_.Package -eq $package }).Count) Templates)" + value = $package + type = 'tag' + templateCount = ($RawTemplates | Where-Object { $_.Package -eq $package }).Count + templates = ($RawTemplates | Where-Object { $_.Package -eq $package } | ForEach-Object { + try { + $JSONData = $_.JSON | ConvertFrom-Json -Depth 100 -ErrorAction SilentlyContinue + $data = $JSONData.RAWJson | ConvertFrom-Json -Depth 100 -ErrorAction SilentlyContinue + $data | Add-Member -NotePropertyName 'displayName' -NotePropertyValue $JSONData.Displayname -Force + $data | Add-Member -NotePropertyName 'description' -NotePropertyValue $JSONData.Description -Force + $data | Add-Member -NotePropertyName 'Type' -NotePropertyValue $JSONData.Type -Force + $data | Add-Member -NotePropertyName 'GUID' -NotePropertyValue $_.RowKey -Force + $data | Add-Member -NotePropertyName 'package' -NotePropertyValue $_.Package -Force + $data + } catch { + + } + }) + } + } | Sort-Object -Property label -Unique + } else { + $Templates = $RawTemplates.JSON | ForEach-Object { try { ConvertFrom-Json -InputObject $_ -Depth 100 -ErrorAction SilentlyContinue } catch {} } + + } } if ($Request.query.ID) { $Templates = $Templates | Where-Object -Property guid -EQ $Request.query.id } 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 8a14f5d0928a..ee0de43333b1 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 @@ -19,71 +19,15 @@ function Invoke-AddGroup { $Results = foreach ($tenant in $SelectedTenants) { try { - $Email = if ($GroupObject.primDomain.value) { "$($GroupObject.username)@$($GroupObject.primDomain.value)" } else { "$($GroupObject.username)@$($tenant)" } - if ($GroupObject.groupType -in 'Generic', 'azurerole', 'dynamic', 'm365') { + # Use the centralized New-CIPPGroup function + $Result = New-CIPPGroup -GroupObject $GroupObject -TenantFilter $tenant -APIName $APIName -ExecutingUser $Request.Headers.'x-ms-client-principal-name' - $BodyParams = [pscustomobject] @{ - 'displayName' = $GroupObject.displayName - 'description' = $GroupObject.description - 'mailNickname' = $GroupObject.username - mailEnabled = [bool]$false - securityEnabled = [bool]$true - isAssignableToRole = [bool]($GroupObject | Where-Object -Property groupType -EQ 'AzureRole') - } - if ($GroupObject.membershipRules) { - $BodyParams | Add-Member -NotePropertyName 'membershipRule' -NotePropertyValue ($GroupObject.membershipRules) - $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 - } 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 -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 + if ($Result.Success) { + "Successfully created group $($GroupObject.displayName) for $($tenant)" + $StatusCode = [HttpStatusCode]::OK } else { - if ($GroupObject.groupType -eq 'dynamicDistribution') { - $ExoParams = @{ - Name = $GroupObject.displayName - RecipientFilter = $GroupObject.membershipRules - PrimarySmtpAddress = $Email - } - $GraphRequest = New-ExoRequest -tenantid $tenant -cmdlet 'New-DynamicDistributionGroup' -cmdParams $ExoParams - } else { - $ExoParams = @{ - Name = $GroupObject.displayName - Alias = $GroupObject.username - Description = $GroupObject.description - PrimarySmtpAddress = $Email - Type = $GroupObject.groupType - RequireSenderAuthenticationEnabled = [bool]!$GroupObject.allowExternal - } - if ($GroupObject.owners) { - $ExoParams.ManagedBy = @($GroupObject.owners.value) - } - if ($GroupObject.members) { - $ExoParams.Members = @($GroupObject.members.value) - } - $GraphRequest = New-ExoRequest -tenantid $tenant -cmdlet 'New-DistributionGroup' -cmdParams $ExoParams - } + throw $Result.Message } - - "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 - $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 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 51380b38dc68..d04461e38d22 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 @@ -15,26 +15,41 @@ 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' } - '*security*' { 'security' } + if (!$Request.Body.displayName) { + throw 'You must enter a displayname' + } + + # Normalize group type to match New-CIPPGroup expectations (handle both camelCase and lowercase) + $groupType = switch -wildcard ($Request.Body.groupType.ToLower()) { + '*dynamicdistribution*' { 'dynamicDistribution'; break } # Check this first before *dynamic* and *distribution* + '*dynamic*' { 'dynamic'; break } + '*azurerole*' { 'azureRole'; break } + '*unified*' { 'm365'; break } + '*microsoft*' { 'm365'; break } + '*m365*' { 'm365'; break } + '*generic*' { 'generic'; break } + '*security*' { 'security'; break } + '*distribution*' { 'distribution'; break } + '*mail*' { 'distribution'; break } default { $Request.Body.groupType } } - if ($Request.body.membershipRules) { $groupType = 'dynamic' } + + # Override to dynamic if membership rules are provided (for backward compatibility) + # but only if it's not already a dynamicDistribution group + if ($Request.body.membershipRules -and $groupType -notin @('dynamicDistribution')) { + $groupType = 'dynamic' + } + # Normalize field names to handle different casing from various forms + $displayName = $Request.Body.displayName ?? $Request.Body.Displayname ?? $Request.Body.displayname + $description = $Request.Body.description ?? $Request.Body.Description + $object = [PSCustomObject]@{ - displayName = $Request.Body.displayName - description = $Request.Body.description + displayName = $displayName + description = $description groupType = $groupType membershipRules = $Request.Body.membershipRules allowExternal = $Request.Body.allowExternal - username = $Request.Body.username + username = $Request.Body.username # Can contain variables like @%tenantfilter% GUID = $GUID } | ConvertTo-Json $Table = Get-CippTable -tablename 'templates' @@ -44,7 +59,7 @@ function Invoke-AddGroupTemplate { RowKey = "$GUID" PartitionKey = 'GroupTemplate' } - Write-LogMessage -headers $Request.Headers -API $APINAME -message "Created Group template named $($Request.Body.displayname) with GUID $GUID" -Sev 'Debug' + Write-LogMessage -headers $Request.Headers -API $APINAME -message "Created Group template named $displayName with GUID $GUID" -Sev 'Debug' $body = [pscustomobject]@{'Results' = 'Successfully added template' } } catch { diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroupTemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroupTemplates.ps1 index 2e9364e73535..8a4d4b0034dd 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroupTemplates.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroupTemplates.ps1 @@ -22,10 +22,26 @@ function Invoke-ListGroupTemplates { $Filter = "PartitionKey eq 'GroupTemplate'" $Templates = (Get-CIPPAzDataTableEntity @Table -Filter $Filter) | ForEach-Object { $data = $_.JSON | ConvertFrom-Json + + # Normalize groupType to camelCase for consistent frontend handling + $normalizedGroupType = switch -Wildcard ($data.groupType.ToLower()) { + '*dynamicdistribution*' { 'dynamicDistribution'; break } + '*dynamic*' { 'dynamic'; break } + '*azurerole*' { 'azureRole'; break } + '*unified*' { 'm365'; break } + '*microsoft*' { 'm365'; break } + '*m365*' { 'm365'; break } + '*generic*' { 'generic'; break } + '*security*' { 'security'; break } + '*distribution*' { 'distribution'; break } + '*mail*' { 'distribution'; break } + default { $data.groupType } + } + [PSCustomObject]@{ displayName = $data.displayName description = $data.description - groupType = $data.groupType + groupType = $normalizedGroupType membershipRules = $data.membershipRules allowExternal = $data.allowExternal username = $data.username 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 2b06bf09a7c7..82ffd30f340d 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 @@ -70,11 +70,12 @@ function Invoke-ExecJITAdmin { $QueueReference = '{0}-{1}' -f $Request.Query.TenantFilter, $PartitionKey # $TenantFilter is 'AllTenants' Write-Information "QueueReference: $QueueReference" - $RunningQueue = Invoke-ListCippQueue | Where-Object { $_.Reference -eq $QueueReference -and $_.Status -notmatch 'Completed' -and $_.Status -notmatch 'Failed' } + $RunningQueue = Invoke-ListCippQueue -Reference $QueueReference | Where-Object { $_.Status -notmatch 'Completed' -and $_.Status -notmatch 'Failed' } if ($RunningQueue) { $Metadata = [PSCustomObject]@{ QueueMessage = 'Still loading JIT Admin data for all tenants. Please check back in a few more minutes.' + QueueId = $RunningQueue.RowKey } } elseif (!$Rows -and !$RunningQueue) { $TenantList = Get-Tenants -IncludeErrors @@ -82,6 +83,7 @@ function Invoke-ExecJITAdmin { $Metadata = [PSCustomObject]@{ QueueMessage = 'Loading JIT Admin data for all tenants. Please check back in a few minutes.' + QueueId = $Queue.RowKey } $InputObject = [PSCustomObject]@{ OrchestratorName = 'JITAdminOrchestrator' @@ -97,6 +99,9 @@ function Invoke-ExecJITAdmin { } Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) } else { + $Metadata = [PSCustomObject]@{ + QueueId = $RunningQueue.RowKey ?? $null + } # There is data in the cache, so we will use that Write-Information "Found $($Rows.Count) rows in the cache" foreach ($row in $Rows) { diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecPerUserMFA.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecPerUserMFA.ps1 index c22b0945e193..46f0e717da95 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecPerUserMFA.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecPerUserMFA.ps1 @@ -6,17 +6,21 @@ function Invoke-ExecPerUserMFA { .ROLE Identity.User.ReadWrite #> - Param($Request, $TriggerMetadata) + param($Request, $TriggerMetadata) $APIName = $Request.Params.CIPPEndpoint $Headers = $Request.Headers Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + # Guest user handling + $UserId = $Request.Body.userPrincipalName -match '#EXT#' ? $Request.Body.userId : $Request.Body.userPrincipalName + $TenantFilter = $Request.Body.tenantFilter + $State = $Request.Body.State.value ? $Request.Body.State.value : $Request.Body.State $Request = @{ - userId = $Request.Body.userId - TenantFilter = $Request.Body.tenantFilter - State = $Request.Body.State.value ? $Request.Body.State.value : $Request.Body.State + userId = $UserId + TenantFilter = $TenantFilter + State = $State Headers = $Headers APIName = $APIName } 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 1cfbd218703c..c0030f1135bc 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 @@ -36,6 +36,15 @@ function Invoke-ListUserSettings { } } } + + try { + $UserSpecificSettings = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq 'UserSettings' and RowKey eq '$Username'" + $UserSpecificSettings = $UserSpecificSettings.JSON | ConvertFrom-Json -Depth 10 -ErrorAction SilentlyContinue + } + catch { + Write-Warning "Failed to convert UserSpecificSettings JSON: $($_.Exception.Message)" + } + #Get branding settings if ($UserSettings) { $brandingTable = Get-CippTable -tablename 'Config' @@ -44,6 +53,11 @@ function Invoke-ListUserSettings { $UserSettings | Add-Member -MemberType NoteProperty -Name 'customBranding' -Value $BrandingSettings -Force | Out-Null } } + + if ($UserSpecificSettings) { + $UserSettings | Add-Member -MemberType NoteProperty -Name 'UserSpecificSettings' -Value $UserSpecificSettings -Force | Out-Null + } + $StatusCode = [HttpStatusCode]::OK $Results = $UserSettings } catch { diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecAlertsList.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecAlertsList.ps1 index 04f876786b21..3671f073f2f1 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecAlertsList.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecAlertsList.ps1 @@ -1,6 +1,6 @@ using namespace System.Net -Function Invoke-ExecAlertsList { +function Invoke-ExecAlertsList { <# .FUNCTIONALITY Entrypoint @@ -63,11 +63,12 @@ Function Invoke-ExecAlertsList { $Filter = "PartitionKey eq '$PartitionKey'" $Rows = Get-CIPPAzDataTableEntity @Table -filter $Filter | Where-Object -Property Timestamp -GT (Get-Date).AddMinutes(-30) $QueueReference = '{0}-{1}' -f $TenantFilter, $PartitionKey - $RunningQueue = Invoke-ListCippQueue | Where-Object { $_.Reference -eq $QueueReference -and $_.Status -notmatch 'Completed' -and $_.Status -notmatch 'Failed' } + $RunningQueue = Invoke-ListCippQueue -Reference $QueueReference | Where-Object { $_.Status -notmatch 'Completed' -and $_.Status -notmatch 'Failed' } # If a queue is running, we will not start a new one if ($RunningQueue) { $Metadata = [PSCustomObject]@{ QueueMessage = 'Still loading data for all tenants. Please check back in a few more minutes' + QueueId = $RunningQueue.RowKey } [PSCustomObject]@{ Waiting = $true @@ -78,6 +79,7 @@ Function Invoke-ExecAlertsList { $Queue = New-CippQueueEntry -Name 'Alerts List - All Tenants' -Reference $QueueReference -TotalTasks ($TenantList | Measure-Object).Count $Metadata = [PSCustomObject]@{ QueueMessage = 'Loading data for all tenants. Please check back in a few minutes' + QueueId = $Queue.RowKey } $InputObject = [PSCustomObject]@{ OrchestratorName = 'AlertsOrchestrator' @@ -97,6 +99,10 @@ Function Invoke-ExecAlertsList { InstanceId = $InstanceId } } else { + $Metadata = [PSCustomObject]@{ + QueueId = $RunningQueue.RowKey ?? $null + } + $Alerts = $Rows $AlertsObj = foreach ($Alert in $Alerts) { $AlertInfo = $Alert.Alert | ConvertFrom-Json diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecIncidentsList.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecIncidentsList.ps1 index 5a3991b4df41..0a848352323d 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecIncidentsList.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecIncidentsList.ps1 @@ -1,6 +1,6 @@ using namespace System.Net -Function Invoke-ExecIncidentsList { +function Invoke-ExecIncidentsList { <# .FUNCTIONALITY Entrypoint @@ -47,11 +47,12 @@ Function Invoke-ExecIncidentsList { $Filter = "PartitionKey eq '$PartitionKey'" $Rows = Get-CIPPAzDataTableEntity @Table -filter $Filter | Where-Object -Property Timestamp -GT (Get-Date).AddMinutes(-30) $QueueReference = '{0}-{1}' -f $TenantFilter, $PartitionKey - $RunningQueue = Invoke-ListCippQueue | Where-Object { $_.Reference -eq $QueueReference -and $_.Status -notmatch 'Completed' -and $_.Status -notmatch 'Failed' } + $RunningQueue = Invoke-ListCippQueue -Reference $QueueReference | Where-Object { $_.Status -notmatch 'Completed' -and $_.Status -notmatch 'Failed' } # If a queue is running, we will not start a new one if ($RunningQueue) { $Metadata = [PSCustomObject]@{ QueueMessage = 'Still loading data for all tenants. Please check back in a few more minutes' + QueueId = $RunningQueue.RowKey } } elseif (!$Rows -and !$RunningQueue) { # If no rows are found and no queue is running, we will start a new one @@ -59,6 +60,7 @@ Function Invoke-ExecIncidentsList { $Queue = New-CippQueueEntry -Name 'Incidents - All Tenants' -Link '/security/reports/incident-report?customerId=AllTenants' -Reference $QueueReference -TotalTasks ($TenantList | Measure-Object).Count $Metadata = [PSCustomObject]@{ QueueMessage = 'Loading data for all tenants. Please check back in a few minutes' + QueueId = $Queue.RowKey } $InputObject = [PSCustomObject]@{ OrchestratorName = 'IncidentOrchestrator' @@ -74,6 +76,9 @@ Function Invoke-ExecIncidentsList { } Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) | Out-Null } else { + $Metadata = [PSCustomObject]@{ + QueueId = $RunningQueue.RowKey ?? $null + } $Incidents = $Rows foreach ($incident in $Incidents) { $IncidentObj = $incident.Incident | ConvertFrom-Json diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecMdoAlertsList.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecMdoAlertsList.ps1 new file mode 100644 index 000000000000..0427a09ab5df --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecMdoAlertsList.ps1 @@ -0,0 +1,84 @@ +using namespace System.Net + +function Invoke-ExecMDOAlertsList { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.Alert.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 + + try { + $GraphRequest = if ($TenantFilter -ne 'AllTenants') { + # Single tenant functionality + New-GraphGetRequest -uri "https://graph.microsoft.com/beta/security/alerts_v2?`$filter=serviceSource eq 'microsoftDefenderForOffice365'" -tenantid $TenantFilter + } else { + # AllTenants functionality + $Table = Get-CIPPTable -TableName cachealertsandincidents + $PartitionKey = 'MdoAlert' + $Filter = "PartitionKey eq '$PartitionKey'" + $Rows = Get-CIPPAzDataTableEntity @Table -filter $Filter | Where-Object -Property Timestamp -GT (Get-Date).AddMinutes(-30) + $QueueReference = '{0}-{1}' -f $TenantFilter, $PartitionKey + $RunningQueue = Invoke-ListCippQueue -Reference $QueueReference | Where-Object { $_.Status -notmatch 'Completed' -and $_.Status -notmatch 'Failed' } + # If a queue is running, we will not start a new one + if ($RunningQueue) { + $Metadata = [PSCustomObject]@{ + QueueMessage = 'Still loading data for all tenants. Please check back in a few more minutes' + QueueId = $RunningQueue.RowKey + } + } elseif (!$Rows -and !$RunningQueue) { + # If no rows are found and no queue is running, we will start a new one + $TenantList = Get-Tenants -IncludeErrors + $Queue = New-CippQueueEntry -Name 'MDO Alerts - All Tenants' -Link '/security/reports/mdo-alerts?customerId=AllTenants' -Reference $QueueReference -TotalTasks ($TenantList | Measure-Object).Count + $Metadata = [PSCustomObject]@{ + QueueMessage = 'Loading data for all tenants. Please check back in a few minutes' + QueueId = $Queue.RowKey + } + $InputObject = [PSCustomObject]@{ + OrchestratorName = 'MdoAlertsOrchestrator' + QueueFunction = @{ + FunctionName = 'GetTenants' + QueueId = $Queue.RowKey + TenantParams = @{ + IncludeErrors = $true + } + DurableName = 'ExecMdoAlertsListAllTenants' + } + SkipLog = $true + } + Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) | Out-Null + } else { + $Metadata = [PSCustomObject]@{ + QueueId = $RunningQueue.RowKey ?? $null + } + $Alerts = $Rows + foreach ($alert in $Alerts) { + ConvertFrom-Json -InputObject $alert.MdoAlert -Depth 10 + } + } + } + } catch { + $Body = Get-NormalizedError -Message $_.Exception.Message + $StatusCode = [HttpStatusCode]::Forbidden + } + if (!$Body) { + $StatusCode = [HttpStatusCode]::OK + $Body = [PSCustomObject]@{ + Results = @($GraphRequest) + Metadata = $Metadata + } + } + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = $Body + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecSetMdoAlert.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecSetMdoAlert.ps1 new file mode 100644 index 000000000000..2da36eadb959 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecSetMdoAlert.ps1 @@ -0,0 +1,74 @@ +using namespace System.Net + +function Invoke-ExecSetMdoAlert { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.Incident.ReadWrite + #> + [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 + $AlertId = $Request.Query.GUID ?? $Request.Body.GUID + $Status = $Request.Query.Status ?? $Request.Body.Status + $Assigned = $Request.Query.Assigned ?? $Request.Body.Assigned ?? ([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Headers.'x-ms-client-principal')) | ConvertFrom-Json).userDetails + $Classification = $Request.Query.Classification ?? $Request.Body.Classification + $Determination = $Request.Query.Determination ?? $Request.Body.Determination + $Result = '' + $AssignBody = @{} + + try { + # Set received status + if ($null -ne $Status) { + $AssignBody.status = $Status + $Result += 'Set status for incident ' + $AlertId + ' to ' + $Status + } + + # Set received classification and determination + if ($null -ne $Classification) { + if ($null -eq $Determination) { + # Maybe some poindexter tries to send a classification without a determination + throw + } + + $AssignBody.classification = $Classification + $AssignBody.determination = $Determination + $Result += 'Set classification & determination for incident ' + $AlertId + ' to ' + $Classification + ' ' + $Determination + } + + # Set received assignee + if ($null -ne $Assigned) { + $AssignBody.assignedTo = $Assigned + if ($null -eq $Status) { + $Result += 'Set assigned for incident ' + $AlertId + ' to ' + $Assigned + } + } + + # Convert hashtable to JSON + $AssignBodyJson = $AssignBody | ConvertTo-Json -Compress + + $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/security/alerts_v2/$AlertId" -type PATCH -tenantid $TenantFilter -body $AssignBodyJson -asApp $true + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev 'Info' + + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to update incident $AlertId : $($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/Tenant/Administration/Alerts/Invoke-ExecAuditLogSearch.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ExecAuditLogSearch.ps1 index d0fb549f3ca5..db1bd87bf843 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 @@ -16,6 +16,8 @@ function Invoke-ExecAuditLogSearch { switch ($Action) { 'ProcessLogs' { + $Table = Get-CIPPTable -TableName 'AuditLogSearches' + $SearchId = $Request.Query.SearchId ?? $Request.Body.SearchId $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter if (!$SearchId) { @@ -26,20 +28,25 @@ function Invoke-ExecAuditLogSearch { return } - $Search = New-GraphGetRequest -Uri "https://graph.microsoft.com/beta/security/auditLog/queries/$SearchId" -AsApp $true -TenantId $TenantFilter - Write-Information ($Search | ConvertTo-Json -Depth 10) + $Existing = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq 'Search' and RowKey eq '$SearchId' and Tenant eq '$TenantFilter'" + if (!$Existing) { + $Search = New-GraphGetRequest -Uri "https://graph.microsoft.com/beta/security/auditLog/queries/$SearchId" -AsApp $true -TenantId $TenantFilter + Write-Information ($Search | ConvertTo-Json -Depth 10) - $Entity = [PSCustomObject]@{ - PartitionKey = [string]'Search' - RowKey = [string]$SearchId - Tenant = [string]$TenantFilter - DisplayName = [string]$Search.displayName - StartTime = [datetime]$Search.filterStartDateTime - EndTime = [datetime]$Search.filterEndDateTime - Query = [string]($Search | ConvertTo-Json -Compress) - CippStatus = [string]'Pending' + $Entity = [PSCustomObject]@{ + PartitionKey = [string]'Search' + RowKey = [string]$SearchId + Tenant = [string]$TenantFilter + DisplayName = [string]$Search.displayName + StartTime = [datetime]$Search.filterStartDateTime + EndTime = [datetime]$Search.filterEndDateTime + Query = [string]($Search | ConvertTo-Json -Compress) + CippStatus = [string]'Pending' + } + } else { + $Existing.CippStatus = 'Pending' } - $Table = Get-CIPPTable -TableName 'AuditLogSearches' + Add-CIPPAzDataTableEntity @Table -Entity $Entity -Force | Out-Null Write-LogMessage -headers $Headers -API $APIName -message "Queued search for processing: $($Search.displayName)" -Sev 'Info' -tenant $TenantFilter diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-EditTenantOffboardingDefaults.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-EditTenantOffboardingDefaults.ps1 new file mode 100644 index 000000000000..9a6520f91c17 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-EditTenantOffboardingDefaults.ps1 @@ -0,0 +1,81 @@ +using namespace System.Net + +function Invoke-EditTenantOffboardingDefaults { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + Tenant.Config.ReadWrite + #> + [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. + $customerId = $Request.Body.customerId + $offboardingDefaults = $Request.Body.offboardingDefaults + + if (!$customerId) { + $response = @{ + state = 'error' + resultText = 'Customer ID is required' + } + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = $response + }) + return + } + + $PropertiesTable = Get-CippTable -TableName 'TenantProperties' + + try { + # Convert the offboarding defaults to JSON string and ensure it's treated as a string + $jsonValue = [string]($offboardingDefaults | ConvertTo-Json -Compress) + + if ($jsonValue -and $jsonValue -ne '{}' -and $jsonValue -ne 'null' -and $jsonValue -ne '') { + # Save offboarding defaults + $offboardingEntity = @{ + PartitionKey = [string]$customerId + RowKey = [string]'OffboardingDefaults' + Value = [string]$jsonValue + } + $null = Add-CIPPAzDataTableEntity @PropertiesTable -Entity $offboardingEntity -Force + Write-LogMessage -headers $Headers -tenant $customerId -API $APIName -message "Updated tenant offboarding defaults" -Sev 'Info' + + $resultText = 'Tenant offboarding defaults updated successfully' + } else { + # Remove offboarding defaults if empty or null + $Existing = Get-CIPPAzDataTableEntity @PropertiesTable -Filter "PartitionKey eq '$customerId' and RowKey eq 'OffboardingDefaults'" + if ($Existing) { + Remove-AzDataTableEntity @PropertiesTable -Entity $Existing + Write-LogMessage -headers $Headers -tenant $customerId -API $APIName -message "Removed tenant offboarding defaults" -Sev 'Info' + } + + $resultText = 'Tenant offboarding defaults cleared successfully' + } + + $response = @{ + state = 'success' + resultText = $resultText + } + + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = $response + }) + } catch { + Write-LogMessage -headers $Headers -tenant $customerId -API $APINAME -message "Edit Tenant Offboarding Defaults failed. The error is: $($_.Exception.Message)" -Sev 'Error' + $response = @{ + state = 'error' + resultText = $_.Exception.Message + } + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::InternalServerError + Body = $response + }) + } +} 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 624e16be31c7..e07b972ff7e1 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 @@ -24,6 +24,8 @@ function Invoke-ListTenants { $AllTenantSelector = $Request.Query.AllTenantSelector } + $IncludeOffboardingDefaults = $Request.Query.IncludeOffboardingDefaults + # Clear Cache if ($Request.Body.ClearCache -eq $true) { $Results = Remove-CIPPCache -tenantsOnly $Request.Body.TenantsOnly @@ -75,16 +77,46 @@ function Invoke-ListTenants { $Tenants = $Tenants | Where-Object -Property customerId -In $TenantAccess } + # If offboarding defaults are requested, fetch them + if ($IncludeOffboardingDefaults -eq 'true' -and $Tenants) { + $PropertiesTable = Get-CippTable -TableName 'TenantProperties' + + # Get all offboarding defaults for all tenants in one query for performance + $AllOffboardingDefaults = Get-CIPPAzDataTableEntity @PropertiesTable -Filter "RowKey eq 'OffboardingDefaults'" + + # Add offboarding defaults to each tenant + foreach ($Tenant in $Tenants) { + $TenantDefaults = $AllOffboardingDefaults | Where-Object { $_.PartitionKey -eq $Tenant.customerId } + if ($TenantDefaults) { + try { + $Tenant | Add-Member -MemberType NoteProperty -Name 'offboardingDefaults' -Value ($TenantDefaults.Value | ConvertFrom-Json) -Force + } catch { + Write-LogMessage -headers $Headers -API $APIName -message "Failed to parse offboarding defaults for tenant $($Tenant.customerId): $($_.Exception.Message)" -Sev 'Warning' + $Tenant | Add-Member -MemberType NoteProperty -Name 'offboardingDefaults' -Value $null -Force + } + } else { + $Tenant | Add-Member -MemberType NoteProperty -Name 'offboardingDefaults' -Value $null -Force + } + } + } + if ($null -eq $TenantFilter -or $TenantFilter -eq 'null') { $TenantList = [system.collections.generic.list[object]]::new() if ($AllTenantSelector -eq $true) { - $TenantList.Add(@{ - customerId = 'AllTenants' - defaultDomainName = 'AllTenants' - displayName = '*All Tenants' - domains = 'AllTenants' - GraphErrorCount = 0 - }) | Out-Null + $AllTenantsObject = @{ + customerId = 'AllTenants' + defaultDomainName = 'AllTenants' + displayName = '*All Tenants' + domains = 'AllTenants' + GraphErrorCount = 0 + } + + # Add offboarding defaults to AllTenants object if requested + if ($IncludeOffboardingDefaults -eq 'true') { + $AllTenantsObject.offboardingDefaults = $null + } + + $TenantList.Add($AllTenantsObject) | Out-Null if (($Tenants).length -gt 1) { $TenantList.AddRange($Tenants) | Out-Null @@ -105,7 +137,9 @@ function Invoke-ListTenants { @{Name = 'portal_intune'; Expression = { "https://intune.microsoft.com/$($_.defaultDomainName)" } }, @{Name = 'portal_security'; Expression = { "https://security.microsoft.com/?tid=$($_.customerId)" } }, @{Name = 'portal_compliance'; Expression = { "https://purview.microsoft.com/?tid=$($_.customerId)" } }, - @{Name = 'portal_sharepoint'; Expression = { "/api/ListSharePointAdminUrl?tenantFilter=$($_.defaultDomainName)" } } + @{Name = 'portal_sharepoint'; Expression = { "/api/ListSharePointAdminUrl?tenantFilter=$($_.defaultDomainName)" } }, + @{Name = 'portal_platform'; Expression = { "https://admin.powerplatform.microsoft.com/account/login/$($_.customerId)" } }, + @{Name = 'portal_bi'; Expression = { "https://app.powerbi.com/admin-portal?ctid=$($_.customerId)" } } } } else { diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ListCAtemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ListCAtemplates.ps1 index 91a163298ce0..94a1f1bbeb1a 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ListCAtemplates.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ListCAtemplates.ps1 @@ -1,6 +1,6 @@ using namespace System.Net -Function Invoke-ListCAtemplates { +function Invoke-ListCAtemplates { <# .FUNCTIONALITY Entrypoint,AnyTenant @@ -39,9 +39,14 @@ Function Invoke-ListCAtemplates { $Table = Get-CippTable -tablename 'templates' $Filter = "PartitionKey eq 'CATemplate'" $Templates = (Get-CIPPAzDataTableEntity @Table -Filter $Filter) | ForEach-Object { - $data = $_.JSON | ConvertFrom-Json -Depth 100 - $data | Add-Member -NotePropertyName 'GUID' -NotePropertyValue $_.GUID -Force - $data + try { + $row = $_ + $data = $row.JSON | ConvertFrom-Json -Depth 100 -ErrorAction Stop + $data | Add-Member -NotePropertyName 'GUID' -NotePropertyValue $row.GUID -Force + $data + } catch { + Write-Warning "Failed to process CA template: $($row.RowKey) - $($_.Exception.Message)" + } } | Sort-Object -Property displayName if ($Request.query.ID) { $Templates = $Templates | Where-Object -Property GUID -EQ $Request.query.id } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPAccessAssignments.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPAccessAssignments.ps1 index 5da36dcb859d..ca258aed65ea 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPAccessAssignments.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPAccessAssignments.ps1 @@ -32,7 +32,7 @@ function Invoke-ListGDAPAccessAssignments { 'method' = 'GET' } } - $Members = New-GraphBulkRequest -Requests $ContainerMembers -tenantid $TenantFilter -asApp $true -NoAuthCheck $true + $Members = New-GraphBulkRequest -Requests @($ContainerMembers) -tenantid $TenantFilter -asApp $true -NoAuthCheck $true $Results = foreach ($AccessAssignment in $AccessAssignments) { [PSCustomObject]@{ 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 21b8b79272c7..ac4fea3c6f67 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 @@ -17,7 +17,8 @@ function Invoke-ListStandardsCompare { $Table.Filter = "PartitionKey eq '{0}'" -f $TenantFilter } - $Standards = Get-CIPPAzDataTableEntity @Table + $Tenants = Get-Tenants -IncludeErrors + $Standards = Get-CIPPAzDataTableEntity @Table | Where-Object { $_.PartitionKey -in $Tenants.defaultDomainName } #in the results we have objects starting with "standards." All these have to be converted from JSON. Do not do this is its a boolean <#$Results | ForEach-Object { @@ -57,7 +58,7 @@ function Invoke-ListStandardsCompare { $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)) + $Chars.Add([char][Convert]::ToInt32($HexEncodedName.Substring($i, 2), 16)) } $FieldName = "$Prefix$(-join $Chars)" } diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-PublicPhishingCheck.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-PublicPhishingCheck.ps1 index 58976b122f30..1c8d4c61afda 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-PublicPhishingCheck.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-PublicPhishingCheck.ps1 @@ -16,6 +16,10 @@ function Invoke-PublicPhishingCheck { if ($Request.body.Cloned -and $Tenant.customerId -eq $Request.body.TenantId) { Write-AlertMessage -message $Request.body.AlertMessage -sev 'Alert' -tenant $Request.body.TenantId + } elseif ($Request.Body.source -and $Tenant) { + $Message = "Alert received from $($Request.Body.source) for $($Request.body.TenantId)" + Write-Information ($Request.Body | ConvertTo-Json) + Write-AlertMessage -message $Message -sev 'Alert' -tenant $Tenant.customerId -LogData $Request.body } # Associate values to output bindings by calling 'Push-OutputBinding'. diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UserTasksOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UserTasksOrchestrator.ps1 index b887e41d3366..87404edca459 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UserTasksOrchestrator.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UserTasksOrchestrator.ps1 @@ -10,10 +10,39 @@ function Start-UserTasksOrchestrator { $1HourAgo = (Get-Date).AddHours(-1).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') $Filter = "PartitionKey eq 'ScheduledTask' and (TaskState eq 'Planned' or TaskState eq 'Failed - Planned' or (TaskState eq 'Running' and Timestamp lt datetime'$1HourAgo'))" $tasks = Get-CIPPAzDataTableEntity @Table -Filter $Filter + + $RateLimitTable = Get-CIPPTable -tablename 'SchedulerRateLimits' + $RateLimits = Get-CIPPAzDataTableEntity @RateLimitTable -Filter "PartitionKey eq 'SchedulerRateLimits'" + + $CIPPCoreModuleRoot = Get-Module -Name CIPPCore | Select-Object -ExpandProperty ModuleBase + $CIPPRoot = (Get-Item $CIPPCoreModuleRoot).Parent.Parent + $DefaultRateLimits = Get-Content -Path "$CIPPRoot/Config/SchedulerRateLimits.json" | ConvertFrom-Json + $NewRateLimits = foreach ($Limit in $DefaultRateLimits) { + if ($Limit.Command -notin $RateLimits.RowKey) { + @{ + PartitionKey = 'SchedulerRateLimits' + RowKey = $Limit.Command + MaxRequests = $Limit.MaxRequests + } + } + } + + if ($NewRateLimits) { + $null = Add-CIPPAzDataTableEntity @RateLimitTable -Entity $NewRateLimits -Force + $RateLimits = Get-CIPPAzDataTableEntity @RateLimitTable -Filter "PartitionKey eq 'SchedulerRateLimits'" + } + + # Create a hashtable for quick rate limit lookups + $RateLimitLookup = @{} + foreach ($limit in $RateLimits) { + $RateLimitLookup[$limit.RowKey] = $limit.MaxRequests + } + $Batch = [System.Collections.Generic.List[object]]::new() $TenantList = Get-Tenants -IncludeErrors foreach ($task in $tasks) { $tenant = $task.Tenant + $currentUnixTime = [int64](([datetime]::UtcNow) - (Get-Date '1/1/1970')).TotalSeconds if ($currentUnixTime -ge $task.ScheduledTime) { try { @@ -113,21 +142,62 @@ function Start-UserTasksOrchestrator { } } } + + Write-Information 'Batching tasks for execution...' + Write-Information "Total tasks to process: $($Batch.Count)" + if (($Batch | Measure-Object).Count -gt 0) { - # Create queue entry - $Queue = New-CippQueueEntry -Name 'Scheduled Tasks' -TotalTasks ($Batch | Measure-Object).Count - $QueueId = $Queue.RowKey - $Batch = $Batch | Select-Object *, @{Name = 'QueueId'; Expression = { $QueueId } }, @{Name = 'QueueName'; Expression = { '{0} - {1}' -f $_.TaskInfo.Name, ($_.TaskInfo.Tenant -ne 'AllTenants' ? $_.TaskInfo.Tenant : $_.Parameters.TenantFilter) } } - - $InputObject = [PSCustomObject]@{ - OrchestratorName = 'UserTaskOrchestrator' - Batch = @($Batch) - SkipLog = $true + # Group commands by type and apply rate limits + $CommandGroups = $Batch | Group-Object -Property Command + $ProcessedBatches = [System.Collections.Generic.List[object]]::new() + + foreach ($CommandGroup in $CommandGroups) { + $CommandName = $CommandGroup.Name + $Commands = [System.Collections.Generic.List[object]]::new($CommandGroup.Group) + + # Get rate limit for this command (default to 100 if not found) + $MaxItemsPerBatch = if ($RateLimitLookup.ContainsKey($CommandName)) { + $RateLimitLookup[$CommandName] + } else { + 100 + } + + # Split into batches based on rate limit + while ($Commands.Count -gt 0) { + $BatchSize = [Math]::Min($Commands.Count, $MaxItemsPerBatch) + $CommandBatch = [System.Collections.Generic.List[object]]::new() + + for ($i = 0; $i -lt $BatchSize; $i++) { + $CommandBatch.Add($Commands[0]) + $Commands.RemoveAt(0) + } + + $ProcessedBatches.Add($CommandBatch) + } } - #Write-Host ($InputObject | ConvertTo-Json -Depth 10) - if ($PSCmdlet.ShouldProcess('Start-UserTasksOrchestrator', 'Starting User Tasks Orchestrator')) { - Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 10 -Compress) + # Process each batch separately + foreach ($ProcessedBatch in $ProcessedBatches) { + Write-Information "Processing batch with $($ProcessedBatch.Count) tasks..." + Write-Information 'Tasks by command:' + $ProcessedBatch | Group-Object -Property Command | ForEach-Object { + Write-Information " - $($_.Name): $($_.Count)" + } + + # Create queue entry for each batch + $Queue = New-CippQueueEntry -Name "Scheduled Tasks - Batch #$($ProcessedBatches.IndexOf($ProcessedBatch) + 1) of $($ProcessedBatches.Count)" + $QueueId = $Queue.RowKey + $BatchWithQueue = $ProcessedBatch | Select-Object *, @{Name = 'QueueId'; Expression = { $QueueId } }, @{Name = 'QueueName'; Expression = { '{0} - {1}' -f $_.TaskInfo.Name, ($_.TaskInfo.Tenant -ne 'AllTenants' ? $_.TaskInfo.Tenant : $_.Parameters.TenantFilter) } } + + $InputObject = [PSCustomObject]@{ + OrchestratorName = 'UserTaskOrchestrator' + Batch = @($BatchWithQueue) + SkipLog = $true + } + + if ($PSCmdlet.ShouldProcess('Start-UserTasksOrchestrator', 'Starting User Tasks Orchestrator')) { + Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 10 -Compress) + } } } } diff --git a/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 b/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 index 8d68ef26e5f3..364e6c9bdf85 100644 --- a/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 +++ b/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 @@ -58,7 +58,8 @@ function Get-CIPPTenantAlignment { $Standards = if ($TenantFilter) { $AllStandards | Where-Object { $_.PartitionKey -eq $TenantFilter } } else { - $AllStandards + $Tenants = Get-Tenants -IncludeErrors + $AllStandards | Where-Object { $_.PartitionKey -in $Tenants.defaultDomainName } } # Build tenant standards data structure @@ -157,6 +158,22 @@ function Get-CIPPTenantAlignment { ReportingEnabled = $IntuneReportingEnabled } } + if ($IntuneTemplate.'TemplateList-Tags') { + foreach ($Tag in $IntuneTemplate.'TemplateList-Tags') { + Write-Host "Processing Intune Tag: $($Tag.value)" + $IntuneActions = if ($IntuneTemplate.action) { $IntuneTemplate.action } else { @() } + $IntuneReportingEnabled = ($IntuneActions | Where-Object { $_.value -and ($_.value.ToLower() -eq 'report' -or $_.value.ToLower() -eq 'remediate') }).Count -gt 0 + $TemplatesList = Get-CIPPAzDataTableEntity @TemplateTable -Filter $Filter | Where-Object -Property package -EQ $Tag.value + $TemplatesList | ForEach-Object { + $TagStandardId = "standards.IntuneTemplate.$($_.GUID)" + [PSCustomObject]@{ + StandardId = $TagStandardId + ReportingEnabled = $IntuneReportingEnabled + } + } + + } + } } } # Handle Conditional Access templates specially @@ -223,7 +240,7 @@ function Get-CIPPTenantAlignment { [PSCustomObject]@{ StandardName = $StandardKey Compliant = $IsCompliant - StandardValue = ($Value | ConvertTo-Json -Compress) + StandardValue = ($Value | ConvertTo-Json -Depth 100 -Compress) ComplianceStatus = $ComplianceStatus ReportingDisabled = $IsReportingDisabled } diff --git a/Modules/CIPPCore/Public/Get-CIPPBitlockerKey.ps1 b/Modules/CIPPCore/Public/Get-CIPPBitlockerKey.ps1 index 291d5b06f63e..6035975ad1d2 100644 --- a/Modules/CIPPCore/Public/Get-CIPPBitlockerKey.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPBitlockerKey.ps1 @@ -1,22 +1,34 @@ -function Get-CIPPBitlockerKey { +function Get-CIPPBitLockerKey { [CmdletBinding()] param ( - $device, + $Device, $TenantFilter, $APIName = 'Get BitLocker key', $Headers ) try { - $GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/informationProtection/bitlocker/recoveryKeys?`$filter=deviceId eq '$($device)'" -tenantid $TenantFilter | ForEach-Object { - (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/informationProtection/bitlocker/recoveryKeys/$($_.id)?`$select=key" -tenantid $TenantFilter).key + $GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/informationProtection/bitlocker/recoveryKeys?`$filter=deviceId eq '$($Device)'" -tenantid $TenantFilter | + ForEach-Object { + $BitLockerKeyObject = (New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/informationProtection/bitlocker/recoveryKeys/$($_.id)?`$select=key" -tenantid $TenantFilter) + [PSCustomObject]@{ + resultText = "Id: $($_.id) Key: $($BitLockerKeyObject.key)" + copyField = $BitLockerKeyObject.key + state = 'success' + } + } + + if ($GraphRequest.Count -eq 0) { + Write-LogMessage -headers $Headers -API $APIName -message "No BitLocker recovery keys found for $($Device)" -Sev Info -tenant $TenantFilter + return "No BitLocker recovery keys found for $($Device)" } + Write-LogMessage -headers $Headers -API $APIName -message "Retrieved BitLocker recovery keys for $($Device)" -Sev Info -tenant $TenantFilter return $GraphRequest } catch { $ErrorMessage = Get-CippException -Exception $_ - $Result = "Could not retrieve BitLocker recovery key for $($device). Error: $($ErrorMessage.NormalizedError)" - Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Error' -tenant $TenantFilter -LogData $ErrorMessage + $Result = "Could not retrieve BitLocker recovery key for $($Device). 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/Get-CIPPDrift.ps1 b/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 index f08cd5e573ff..bb32924cc8c4 100644 --- a/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 @@ -29,6 +29,24 @@ function Get-CIPPDrift { [switch]$AllTenants ) + + $IntuneTable = Get-CippTable -tablename 'templates' + $IntuneFilter = "PartitionKey eq 'IntuneTemplate'" + $RawIntuneTemplates = (Get-CIPPAzDataTableEntity @IntuneTable -Filter $IntuneFilter) + $AllIntuneTemplates = $RawIntuneTemplates | ForEach-Object { + try { + $JSONData = $_.JSON | ConvertFrom-Json -Depth 100 -ErrorAction SilentlyContinue + $data = $JSONData.RAWJson | ConvertFrom-Json -Depth 100 -ErrorAction SilentlyContinue + $data | Add-Member -NotePropertyName 'displayName' -NotePropertyValue $JSONData.Displayname -Force + $data | Add-Member -NotePropertyName 'description' -NotePropertyValue $JSONData.Description -Force + $data | Add-Member -NotePropertyName 'Type' -NotePropertyValue $JSONData.Type -Force + $data | Add-Member -NotePropertyName 'GUID' -NotePropertyValue $_.RowKey -Force + $data + } catch { + # Skip invalid templates + } + } | Sort-Object -Property displayName + try { $AlignmentData = Get-CIPPTenantAlignment -TenantFilter $TenantFilter -TemplateId $TemplateId | Where-Object -Property standardType -EQ 'drift' if (-not $AlignmentData) { @@ -64,16 +82,24 @@ function Get-CIPPDrift { } else { 'New' } + #if the $ComparisonItem.StandardName contains *intuneTemplate*, then it's an Intune policy deviation, and we need to grab the correct displayname from the template table + if ($ComparisonItem.StandardName -like '*intuneTemplate*') { + $CompareGuid = $ComparisonItem.StandardName.Split('.') | Select-Object -Index 2 + Write-Host "Extracted GUID: $CompareGuid" + $Template = $AllIntuneTemplates | Where-Object { $_.GUID -like "*$CompareGuid*" } + if ($Template) { $displayName = $Template.displayName } + } $reason = if ($ExistingDriftStates.ContainsKey($ComparisonItem.StandardName)) { $ExistingDriftStates[$ComparisonItem.StandardName].Reason } $User = if ($ExistingDriftStates.ContainsKey($ComparisonItem.StandardName)) { $ExistingDriftStates[$ComparisonItem.StandardName].User } $StandardsDeviations.Add([PSCustomObject]@{ - standardName = $ComparisonItem.StandardName - expectedValue = 'Compliant' - receivedValue = $ComparisonItem.StandardValue - state = 'current' - Status = $Status - Reason = $reason - lastChangedByUser = $User + standardName = $ComparisonItem.StandardName + standardDisplayName = $displayName + expectedValue = 'Compliant' + receivedValue = $ComparisonItem.StandardValue + state = 'current' + Status = $Status + Reason = $reason + lastChangedByUser = $User }) } } @@ -194,22 +220,6 @@ function Get-CIPPDrift { # Get actual Intune templates from templates table if ($IntuneTemplateIds.Count -gt 0) { try { - $IntuneTable = Get-CippTable -tablename 'templates' - $IntuneFilter = "PartitionKey eq 'IntuneTemplate'" - $RawIntuneTemplates = (Get-CIPPAzDataTableEntity @IntuneTable -Filter $IntuneFilter) - $AllIntuneTemplates = $RawIntuneTemplates | ForEach-Object { - try { - $JSONData = $_.JSON | ConvertFrom-Json -Depth 100 -ErrorAction SilentlyContinue - $data = $JSONData.RAWJson | ConvertFrom-Json -Depth 100 -ErrorAction SilentlyContinue - $data | Add-Member -NotePropertyName 'displayName' -NotePropertyValue $JSONData.Displayname -Force - $data | Add-Member -NotePropertyName 'description' -NotePropertyValue $JSONData.Description -Force - $data | Add-Member -NotePropertyName 'Type' -NotePropertyValue $JSONData.Type -Force - $data | Add-Member -NotePropertyName 'GUID' -NotePropertyValue $_.RowKey -Force - $data - } catch { - # Skip invalid templates - } - } | Sort-Object -Property displayName $TemplateIntuneTemplates = $AllIntuneTemplates | Where-Object { $_.GUID -in $IntuneTemplateIds } } catch { @@ -222,7 +232,6 @@ function Get-CIPPDrift { $PolicyFound = $false $tenantPolicy.policy | Add-Member -MemberType NoteProperty -Name 'URLName' -Value $TenantPolicy.Type -Force $TenantPolicyName = if ($TenantPolicy.Policy.displayName) { $TenantPolicy.Policy.displayName } else { $TenantPolicy.Policy.name } - foreach ($TemplatePolicy in $TemplateIntuneTemplates) { $TemplatePolicyName = if ($TemplatePolicy.displayName) { $TemplatePolicy.displayName } else { $TemplatePolicy.name } diff --git a/Modules/CIPPCore/Public/Get-CIPPTextReplacement.ps1 b/Modules/CIPPCore/Public/Get-CIPPTextReplacement.ps1 index 68e801215b8a..54e7e11e0923 100644 --- a/Modules/CIPPCore/Public/Get-CIPPTextReplacement.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPTextReplacement.ps1 @@ -36,7 +36,10 @@ function Get-CIPPTextReplacement { '%windir%', '%programfiles%', '%programfiles(x86)%', - '%programdata%' + '%programdata%', + '%cippuserschema%', + '%cippurl%', + '%defaultdomain%' ) $Tenant = Get-Tenants -TenantFilter $TenantFilter @@ -71,11 +74,25 @@ function Get-CIPPTextReplacement { #default replacements for all tenants: %tenantid% becomes $tenant.customerId, %tenantfilter% becomes $tenant.defaultDomainName, %tenantname% becomes $tenant.displayName $Text = $Text -replace '%tenantid%', $Tenant.customerId $Text = $Text -replace '%tenantfilter%', $Tenant.defaultDomainName + $Text = $Text -replace '%defaultdomain%', $Tenant.defaultDomainName $Text = $Text -replace '%initialdomain%', $Tenant.initialDomainName $Text = $Text -replace '%tenantname%', $Tenant.displayName # Partner specific replacements $Text = $Text -replace '%partnertenantid%', $env:TenantID $Text = $Text -replace '%samappid%', $env:ApplicationID + + if ($Text -match '%cippuserschema%') { + $Schema = Get-CIPPSchemaExtensions | Where-Object { $_.id -match '_cippUser' } | Select-Object -First 1 + $Text = $Text -replace '%cippuserschema%', $Schema.id + } + + if ($Text -match '%cippurl%') { + $ConfigTable = Get-CIPPTable -tablename 'Config' + $Config = Get-CIPPAzDataTableEntity @ConfigTable -Filter "PartitionKey eq 'InstanceProperties' and RowKey eq 'CIPPURL'" + if ($Config) { + $Text = $Text -replace '%cippurl%', $Config.Value + } + } return $Text } diff --git a/Modules/CIPPCore/Public/GraphHelper/Get-Tenants.ps1 b/Modules/CIPPCore/Public/GraphHelper/Get-Tenants.ps1 index c231eb916100..b2ae8f4555cc 100644 --- a/Modules/CIPPCore/Public/GraphHelper/Get-Tenants.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/Get-Tenants.ps1 @@ -42,8 +42,8 @@ function Get-Tenants { $IncludedTenantFilter = [scriptblock]::Create("`$_.customerId -eq '$TenantFilter'") $RelationshipFilter = " and customer/tenantId eq '$TenantFilter'" } else { - $Filter = "{0} and defaultDomainName eq '{1}'" -f $Filter, $TenantFilter - $IncludedTenantFilter = [scriptblock]::Create("`$_.defaultDomainName -eq '$TenantFilter'") + $Filter = "{0} and defaultDomainName eq '{1}' or initialDomainName eq '{1}'" -f $Filter, $TenantFilter + $IncludedTenantFilter = [scriptblock]::Create("`$_.defaultDomainName -eq '$TenantFilter' -or `$_.initialDomainName -eq '$TenantFilter'") $RelationshipFilter = '' } } else { diff --git a/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 index e7a3ad3bdf9b..e1281bff1c28 100644 --- a/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 @@ -56,71 +56,108 @@ function New-GraphGetRequest { } $ReturnedData = do { - try { - $GraphRequest = @{ - Uri = $nextURL - Method = 'GET' - Headers = $headers - ContentType = 'application/json; charset=utf-8' - } - if ($IncludeResponseHeaders) { - $GraphRequest.ResponseHeadersVariable = 'ResponseHeaders' - } - - if ($ReturnRawResponse) { - $GraphRequest.SkipHttpErrorCheck = $true - $Data = Invoke-WebRequest @GraphRequest - } else { - $Data = (Invoke-RestMethod @GraphRequest) - } + $RetryCount = 0 + $MaxRetries = 3 + $RequestSuccessful = $false + Write-Host "This is attempt $($RetryCount + 1) of $MaxRetries" + do { + try { + $GraphRequest = @{ + Uri = $nextURL + Method = 'GET' + Headers = $headers + ContentType = 'application/json; charset=utf-8' + } + if ($IncludeResponseHeaders) { + $GraphRequest.ResponseHeadersVariable = 'ResponseHeaders' + } - if ($ReturnRawResponse) { - if (Test-Json -Json $Data.Content) { - $Content = $Data.Content | ConvertFrom-Json + if ($ReturnRawResponse) { + $GraphRequest.SkipHttpErrorCheck = $true + $Data = Invoke-WebRequest @GraphRequest } else { - $Content = $Data.Content + $Data = (Invoke-RestMethod @GraphRequest) } - $Data | Select-Object -Property StatusCode, StatusDescription, @{Name = 'Content'; Expression = { $Content }} - $nextURL = $null - } elseif ($CountOnly) { - $Data.'@odata.count' - $NextURL = $null - } else { - if ($Data.PSObject.Properties.Name -contains 'value') { $data.value } else { $Data } - if ($noPagination -eq $true) { - if ($Caller -eq 'Get-GraphRequestList') { - @{ 'nextLink' = $data.'@odata.nextLink' } + # If we reach here, the request was successful + $RequestSuccessful = $true + + if ($ReturnRawResponse) { + if (Test-Json -Json $Data.Content) { + $Content = $Data.Content | ConvertFrom-Json + } else { + $Content = $Data.Content } + + $Data | Select-Object -Property StatusCode, StatusDescription, @{Name = 'Content'; Expression = { $Content } } $nextURL = $null + } elseif ($CountOnly) { + $Data.'@odata.count' + $NextURL = $null } else { - $NextPageUriFound = $false - if ($IncludeResponseHeaders) { - if ($ResponseHeaders.NextPageUri) { - $NextURL = $ResponseHeaders.NextPageUri - $NextPageUriFound = $true + if ($Data.PSObject.Properties.Name -contains 'value') { $data.value } else { $Data } + if ($noPagination -eq $true) { + if ($Caller -eq 'Get-GraphRequestList') { + @{ 'nextLink' = $data.'@odata.nextLink' } + } + $nextURL = $null + } else { + $NextPageUriFound = $false + if ($IncludeResponseHeaders) { + if ($ResponseHeaders.NextPageUri) { + $NextURL = $ResponseHeaders.NextPageUri + $NextPageUriFound = $true + } + } + if (!$NextPageUriFound) { + $nextURL = $data.'@odata.nextLink' } } - if (!$NextPageUriFound) { - $nextURL = $data.'@odata.nextLink' + } + } catch { + $ShouldRetry = $false + $WaitTime = 0 + + try { + $Message = ($_.ErrorDetails.Message | ConvertFrom-Json -ErrorAction SilentlyContinue).error.message + } catch { $Message = $null } + if ($Message -eq $null) { $Message = $($_.Exception.Message) } + + # Check for 429 Too Many Requests + if ($_.Exception.Response.StatusCode -eq 429) { + $RetryAfterHeader = $_.Exception.Response.Headers['Retry-After'] + if ($RetryAfterHeader) { + $WaitTime = [int]$RetryAfterHeader + Write-Warning "Rate limited (429). Waiting $WaitTime seconds before retry. Attempt $($RetryCount + 1) of $MaxRetries" + $ShouldRetry = $true } } - } - } catch { - try { - $Message = ($_.ErrorDetails.Message | ConvertFrom-Json -ErrorAction SilentlyContinue).error.message - } catch { $Message = $null } - if ($Message -eq $null) { $Message = $($_.Exception.Message) } - if ($Message -ne 'Request not applicable to target tenant.' -and $Tenant) { - $Tenant.LastGraphError = $Message - if ($Tenant.PSObject.Properties.Name -notcontains 'GraphErrorCount') { - $Tenant | Add-Member -MemberType NoteProperty -Name 'GraphErrorCount' -Value 0 -Force + # Check for "Resource temporarily unavailable" + elseif ($Message -like '*Resource temporarily unavailable*') { + if ($RetryCount -lt $MaxRetries) { + $WaitTime = Get-Random -Minimum 1 -Maximum 10 # Random sleep between 1-10 seconds + Write-Warning "Resource temporarily unavailable. Waiting $WaitTime seconds before retry. Attempt $($RetryCount + 1) of $MaxRetries" + $ShouldRetry = $true + } + } + + if ($ShouldRetry -and $RetryCount -lt $MaxRetries) { + $RetryCount++ + Start-Sleep -Seconds $WaitTime + } else { + # Final failure - update tenant error tracking and throw + if ($Message -ne 'Request not applicable to target tenant.' -and $Tenant) { + $Tenant.LastGraphError = $Message + if ($Tenant.PSObject.Properties.Name -notcontains 'GraphErrorCount') { + $Tenant | Add-Member -MemberType NoteProperty -Name 'GraphErrorCount' -Value 0 -Force + } + $Tenant.GraphErrorCount++ + Update-AzDataTableEntity -Force @TenantsTable -Entity $Tenant + } + throw $Message } - $Tenant.GraphErrorCount++ - Update-AzDataTableEntity -Force @TenantsTable -Entity $Tenant } - throw $Message - } + } while (-not $RequestSuccessful -and $RetryCount -le $MaxRetries) } until ([string]::IsNullOrEmpty($NextURL) -or $NextURL -is [object[]] -or ' ' -eq $NextURL) if ($Tenant.PSObject.Properties.Name -notcontains 'LastGraphError') { $Tenant | Add-Member -MemberType NoteProperty -Name 'LastGraphError' -Value '' -Force diff --git a/Modules/CIPPCore/Public/GraphHelper/Write-AlertMessage.ps1 b/Modules/CIPPCore/Public/GraphHelper/Write-AlertMessage.ps1 index a7a19d9b6340..30eea9247a42 100644 --- a/Modules/CIPPCore/Public/GraphHelper/Write-AlertMessage.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/Write-AlertMessage.ps1 @@ -1,4 +1,4 @@ -function Write-AlertMessage($message, $tenant = 'None', $tenantId = $null) { +function Write-AlertMessage($message, $tenant = 'None', $tenantId = $null, $LogData = @{}) { <# .FUNCTIONALITY Internal @@ -10,7 +10,7 @@ function Write-AlertMessage($message, $tenant = 'None', $tenantId = $null) { $ExistingMessage = Get-CIPPAzDataTableEntity @Table -Filter $Filter if (!$ExistingMessage) { Write-Host 'No duplicate message found, writing to log' - Write-LogMessage -message $message -tenant $tenant -sev 'Alert' -tenantId $tenantId -API 'Alerts' + Write-LogMessage -message $message -tenant $tenant -sev 'Alert' -tenantId $tenantId -API 'Alerts' -LogData $LogData } else { Write-Host 'Alerts: Duplicate entry found, not writing to log' diff --git a/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 b/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 index 1370345fe9a1..0f31743a02bf 100644 --- a/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 +++ b/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 @@ -33,6 +33,9 @@ function Get-GraphRequestList { .PARAMETER NoPagination Disable pagination + .PARAMETER ManualPagination + Enable manual pagination using nextLink + .PARAMETER CountOnly Only return count of results @@ -45,6 +48,15 @@ function Get-GraphRequestList { .PARAMETER ReverseTenantLookupProperty Property to perform reverse tenant lookup + .PARAMETER AsApp + Run the request as an application + + .PARAMETER Caller + Name of the calling function + + .PARAMETER UseBatchExpand + Perform a batch lookup using the $expand query parameter to avoid 20 item max + #> [CmdletBinding()] param( @@ -61,11 +73,14 @@ function Get-GraphRequestList { [switch]$SkipCache, [switch]$ClearCache, [switch]$NoPagination, + [switch]$ManualPagination, [switch]$CountOnly, [switch]$NoAuthCheck, [switch]$ReverseTenantLookup, [string]$ReverseTenantLookupProperty = 'tenantId', - [boolean]$AsApp = $false + [boolean]$AsApp = $false, + [string]$Caller = 'Get-GraphRequestList', + [switch]$UseBatchExpand ) $SingleTenantThreshold = 8000 @@ -89,7 +104,21 @@ function Get-GraphRequestList { $Item.Value = $Item.Value.ToString().ToLower() } if ($Item.Value) { - $ParamCollection.Add($Item.Key, $Item.Value) + if ($Item.Key -eq '$select' -or $Item.Key -eq 'select') { + $Columns = $Item.Value -split ',' + $ActualCols = foreach ($Col in $Columns) { + $Col -split '\.' | Select-Object -First 1 + } + $Value = ($ActualCols | Sort-Object -Unique) -join ',' + } else { + $Value = $Item.Value + } + + if ($UseBatchExpand.IsPresent -and ($Item.Key -eq '$expand' -or $Item.Key -eq 'expand')) { + $BatchExpandQuery = $Item.Value + } else { + $ParamCollection.Add($Item.Key, $Value) + } } } $GraphQuery.Query = $ParamCollection.ToString() @@ -104,8 +133,8 @@ function Get-GraphRequestList { tenantid = $TenantFilter ComplexFilter = $true } - if ($NoPagination.IsPresent) { - $GraphRequest.noPagination = $NoPagination.IsPresent + if ($NoPagination.IsPresent -or $ManualPagination.IsPresent) { + $GraphRequest.noPagination = $true } if ($CountOnly.IsPresent) { $GraphRequest.CountOnly = $CountOnly.IsPresent @@ -123,7 +152,16 @@ function Get-GraphRequestList { $GraphQuery = [System.UriBuilder]('https://graph.microsoft.com/{0}/{1}' -f $Version, $Endpoint) $ParamCollection = [System.Web.HttpUtility]::ParseQueryString([String]::Empty) foreach ($Item in ($Parameters.GetEnumerator() | Sort-Object -CaseSensitive -Property Key)) { - $Value = Get-CIPPTextReplacement -TenantFilter $TenantFilter -Text $Item.Value + if ($Item.Key -eq '$select' -or $Item.Key -eq 'select') { + $Columns = $Item.Value -split ',' + $ActualCols = foreach ($Col in $Columns) { + $Col -split '\.' | Select-Object -First 1 + } + $Value = ($ActualCols | Sort-Object -Unique) -join ',' + } else { + $Value = $Item.Value + } + $Value = Get-CIPPTextReplacement -TenantFilter $TenantFilter -Text $Value $ParamCollection.Add($Item.Key, $Value) } $GraphQuery.Query = $ParamCollection.ToString() @@ -146,7 +184,7 @@ function Get-GraphRequestList { $Type = 'Queue' Write-Information "Cached: $(($Rows | Measure-Object).Count) rows (Type: $($Type))" $QueueReference = '{0}-{1}' -f $TenantFilter, $PartitionKey - $RunningQueue = Invoke-ListCippQueue | Where-Object { $_.Reference -eq $QueueReference -and $_.Status -ne 'Completed' -and $_.Status -ne 'Failed' } + $RunningQueue = Invoke-ListCippQueue -Reference $QueueReference | Where-Object { $_.Status -ne 'Completed' -and $_.Status -ne 'Failed' } } elseif (!$SkipCache.IsPresent -and !$ClearCache.IsPresent -and !$CountOnly.IsPresent) { if ($TenantFilter -eq 'AllTenants' -or $Count -gt $SingleTenantThreshold) { $Table = Get-CIPPTable -TableName $TableName @@ -160,7 +198,7 @@ function Get-GraphRequestList { $Type = 'Cache' Write-Information "Cached: $(($Rows | Measure-Object).Count) rows (Type: $($Type))" $QueueReference = '{0}-{1}' -f $TenantFilter, $PartitionKey - $RunningQueue = Invoke-ListCippQueue | Where-Object { $_.Reference -eq $QueueReference -and $_.Status -notmatch 'Completed' -and $_.Status -notmatch 'Failed' } + $RunningQueue = Invoke-ListCippQueue -Reference $QueueReference | Where-Object { $_.Status -notmatch 'Completed' -and $_.Status -notmatch 'Failed' } } } } catch { @@ -297,11 +335,55 @@ function Get-GraphRequestList { if (!$QueueThresholdExceeded) { #nextLink should ONLY be used in direct calls with manual pagination. It should not be used in queueing - if ($NoPagination.IsPresent -and $nextLink -match '^https://.+') { $GraphRequest.uri = $nextLink } + if ($ManualPagination.IsPresent -and $nextLink -match '^https://.+') { $GraphRequest.uri = $nextLink } - $GraphRequestResults = New-GraphGetRequest @GraphRequest -Caller 'Get-GraphRequestList' -ErrorAction Stop + $GraphRequestResults = New-GraphGetRequest @GraphRequest -Caller $Caller -ErrorAction Stop $GraphRequestResults = $GraphRequestResults | Select-Object *, @{n = 'Tenant'; e = { $TenantFilter } }, @{n = 'CippStatus'; e = { 'Good' } } + if ($UseBatchExpand.IsPresent -and ![string]::IsNullOrEmpty($BatchExpandQuery)) { + if ($BatchExpandQuery -match '' -and ![string]::IsNullOrEmpty($GraphRequestResults.id)) { + # Convert $expand format to actual batch query e.g. members($select=id,displayName) to members?$select=id,displayName + $BatchExpandQuery = $BatchExpandQuery -replace '\(\$?([^=]+)=([^)]+)\)', '?$$$1=$2' -replace ';', '&' + + # Extract property name from expand + $Property = $BatchExpandQuery -replace '\?.*$', '' -replace '^.*\/', '' + Write-Information "Performing batch expansion for property '$Property'..." + + if ($Property -eq 'assignedLicenses') { + $LicenseDetails = Get-CIPPLicenseOverview -TenantFilter $TenantFilter + $GraphRequestResults = foreach ($GraphRequestResult in $GraphRequestResults) { + $NewLicenses = [system.collections.generic.list[string]]::new() + foreach ($License in $GraphRequestResult.assignedLicenses) { + $LicenseInfo = $LicenseDetails | Where-Object { $_.skuId -eq $License.skuId } | Select-Object -First 1 + if ($LicenseInfo) { + $NewLicenses.Add($LicenseInfo.License) + } + } + $GraphRequestResult | Add-Member -MemberType NoteProperty -Name $Property -Value @($NewLicenses) -Force + $GraphRequestResult + } + } else { + + $Uri = "$Endpoint/{0}/$BatchExpandQuery" + + $Requests = foreach ($Result in $GraphRequestResults) { + @{ + id = $Result.id + url = $Uri -f $Result.id + method = 'GET' + } + } + $BatchResults = New-GraphBulkRequest -Requests @($Requests) -tenantid $TenantFilter -NoAuthCheck $NoAuthCheck.IsPresent -asapp $AsApp + + $GraphRequestResults = foreach ($Result in $GraphRequestResults) { + $PropValue = $BatchResults | Where-Object { $_.id -eq $Result.id } | Select-Object -ExpandProperty body + $Result | Add-Member -MemberType NoteProperty -Name $Property -Value ($PropValue.value ?? $PropValue) + $Result + } + } + } + } + if ($ReverseTenantLookup -and $GraphRequestResults) { $ReverseLookupRequests = $GraphRequestResults.$ReverseTenantLookupProperty | Sort-Object -Unique | ForEach-Object { @{ diff --git a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 index 0ec869f6e596..dda1e9829d22 100644 --- a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 @@ -88,17 +88,17 @@ function New-CIPPCAPolicy { $displayname = ($RawJSON | ConvertFrom-Json).Displayname - $JSONObj = $RawJSON | ConvertFrom-Json | Select-Object * -ExcludeProperty ID, GUID, *time* - Remove-EmptyArrays $JSONObj + $JSONobj = $RawJSON | ConvertFrom-Json | Select-Object * -ExcludeProperty ID, GUID, *time* + Remove-EmptyArrays $JSONobj #Remove context as it does not belong in the payload. try { - $JsonObj.grantControls.PSObject.Properties.Remove('authenticationStrength@odata.context') - $JSONObj.templateId ? $JSONObj.PSObject.Properties.Remove('templateId') : $null - if ($JSONObj.conditions.users.excludeGuestsOrExternalUsers.externalTenants.Members) { - $JsonObj.conditions.users.excludeGuestsOrExternalUsers.externalTenants.PSObject.Properties.Remove('@odata.context') + $JSONobj.grantControls.PSObject.Properties.Remove('authenticationStrength@odata.context') + $JSONobj.templateId ? $JSONobj.PSObject.Properties.Remove('templateId') : $null + if ($JSONobj.conditions.users.excludeGuestsOrExternalUsers.externalTenants.Members) { + $JSONobj.conditions.users.excludeGuestsOrExternalUsers.externalTenants.PSObject.Properties.Remove('@odata.context') } if ($State -and $State -ne 'donotchange') { - $Jsonobj.state = $State + $JSONobj.state = $State } } catch { # no issues here. @@ -108,18 +108,18 @@ function New-CIPPCAPolicy { if ($JSONobj.GrantControls.authenticationStrength.policyType -eq 'custom' -or $JSONobj.GrantControls.authenticationStrength.policyType -eq 'BuiltIn') { $ExistingStrength = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/authenticationStrength/policies/' -tenantid $TenantFilter -asApp $true | Where-Object -Property displayName -EQ $JSONobj.GrantControls.authenticationStrength.displayName if ($ExistingStrength) { - $JSONObj.GrantControls.authenticationStrength = @{ id = $ExistingStrength.id } + $JSONobj.GrantControls.authenticationStrength = @{ id = $ExistingStrength.id } } else { - $Body = ConvertTo-Json -InputObject $JSONObj.GrantControls.authenticationStrength + $Body = ConvertTo-Json -InputObject $JSONobj.GrantControls.authenticationStrength $GraphRequest = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/authenticationStrength/policies' -body $body -Type POST -tenantid $tenantfilter -asApp $true - $JSONObj.GrantControls.authenticationStrength = @{ id = $ExistingStrength.id } - Write-LogMessage -Headers $User -API $APINAME -message "Created new Authentication Strength Policy: $($JSONObj.GrantControls.authenticationStrength.displayName)" -Sev 'Info' + $JSONobj.GrantControls.authenticationStrength = @{ id = $ExistingStrength.id } + Write-LogMessage -Headers $User -API $APINAME -message "Created new Authentication Strength Policy: $($JSONobj.GrantControls.authenticationStrength.displayName)" -Sev 'Info' } } - #for each of the locations, check if they exist, if not create them. These are in $jsonobj.LocationInfo - $LocationLookupTable = foreach ($locations in $jsonobj.LocationInfo) { + #for each of the locations, check if they exist, if not create them. These are in $JSONobj.LocationInfo + $LocationLookupTable = foreach ($locations in $JSONobj.LocationInfo) { if (!$locations) { continue } foreach ($location in $locations) { if (!$location.displayName) { continue } @@ -152,20 +152,20 @@ function New-CIPPCAPolicy { } } - foreach ($location in $JSONObj.conditions.locations.includeLocations) { + foreach ($location in $JSONobj.conditions.locations.includeLocations) { Write-Information "Replacing named location - $location" $lookup = $LocationLookupTable | Where-Object -Property name -EQ $location Write-Information "Found $lookup" if (!$lookup) { continue } - $index = [array]::IndexOf($JSONObj.conditions.locations.includeLocations, $location) - $JSONObj.conditions.locations.includeLocations[$index] = $lookup.id + $index = [array]::IndexOf($JSONobj.conditions.locations.includeLocations, $location) + $JSONobj.conditions.locations.includeLocations[$index] = $lookup.id } - foreach ($location in $JSONObj.conditions.locations.excludeLocations) { + foreach ($location in $JSONobj.conditions.locations.excludeLocations) { $lookup = $LocationLookupTable | Where-Object -Property name -EQ $location if (!$lookup) { continue } - $index = [array]::IndexOf($JSONObj.conditions.locations.excludeLocations, $location) - $JSONObj.conditions.locations.excludeLocations[$index] = $lookup.id + $index = [array]::IndexOf($JSONobj.conditions.locations.excludeLocations, $location) + $JSONobj.conditions.locations.excludeLocations[$index] = $lookup.id } switch ($ReplacePattern) { 'none' { @@ -174,10 +174,10 @@ function New-CIPPCAPolicy { } 'AllUsers' { Write-Information 'Replacement pattern for inclusions and exclusions is All users. This policy will now apply to everyone.' - if ($JSONObj.conditions.users.includeUsers -ne 'All') { $JSONObj.conditions.users.includeUsers = @('All') } - if ($JSONObj.conditions.users.excludeUsers) { $JSONObj.conditions.users.excludeUsers = @() } - if ($JSONObj.conditions.users.includeGroups) { $JSONObj.conditions.users.includeGroups = @() } - if ($JSONObj.conditions.users.excludeGroups) { $JSONObj.conditions.users.excludeGroups = @() } + if ($JSONobj.conditions.users.includeUsers -ne 'All') { $JSONobj.conditions.users.includeUsers = @('All') } + if ($JSONobj.conditions.users.excludeUsers) { $JSONobj.conditions.users.excludeUsers = @() } + if ($JSONobj.conditions.users.includeGroups) { $JSONobj.conditions.users.includeGroups = @() } + if ($JSONobj.conditions.users.excludeGroups) { $JSONobj.conditions.users.excludeGroups = @() } } 'displayName' { try { @@ -186,41 +186,41 @@ function New-CIPPCAPolicy { $groups = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/groups?$select=id,displayName' -tenantid $TenantFilter -asApp $true foreach ($userType in 'includeUsers', 'excludeUsers') { - if ($JSONObj.conditions.users.PSObject.Properties.Name -contains $userType -and $JSONObj.conditions.users.$userType -notin 'All', 'None', 'GuestOrExternalUsers') { - $JSONObj.conditions.users.$userType = @(Replace-UserNameWithId -userNames $JSONObj.conditions.users.$userType) + if ($JSONobj.conditions.users.PSObject.Properties.Name -contains $userType -and $JSONobj.conditions.users.$userType -notin 'All', 'None', 'GuestOrExternalUsers') { + $JSONobj.conditions.users.$userType = @(Replace-UserNameWithId -userNames $JSONobj.conditions.users.$userType) } } # Check the included and excluded groups foreach ($groupType in 'includeGroups', 'excludeGroups') { - if ($JSONObj.conditions.users.PSObject.Properties.Name -contains $groupType) { - $JSONObj.conditions.users.$groupType = @(Replace-GroupNameWithId -groupNames $JSONObj.conditions.users.$groupType) + if ($JSONobj.conditions.users.PSObject.Properties.Name -contains $groupType) { + $JSONobj.conditions.users.$groupType = @(Replace-GroupNameWithId -groupNames $JSONobj.conditions.users.$groupType) } } } catch { $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to replace displayNames for conditional access rule $($JSONObj.displayName). Error: $($ErrorMessage.NormalizedError)" -sev 'Error' -LogData $ErrorMessage - throw "Failed to replace displayNames for conditional access rule $($JSONObj.displayName): $($ErrorMessage.NormalizedError)" + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to replace displayNames for conditional access rule $($JSONobj.displayName). Error: $($ErrorMessage.NormalizedError)" -sev 'Error' -LogData $ErrorMessage + throw "Failed to replace displayNames for conditional access rule $($JSONobj.displayName): $($ErrorMessage.NormalizedError)" } } } - $JsonObj.PSObject.Properties.Remove('LocationInfo') - foreach ($condition in $JSONObj.conditions.users.PSObject.Properties.Name) { - $value = $JSONObj.conditions.users.$condition + $JSONobj.PSObject.Properties.Remove('LocationInfo') + foreach ($condition in $JSONobj.conditions.users.PSObject.Properties.Name) { + $value = $JSONobj.conditions.users.$condition if ($null -eq $value) { - $JSONObj.conditions.users.$condition = @() + $JSONobj.conditions.users.$condition = @() continue } if ($value -is [string]) { if ([string]::IsNullOrWhiteSpace($value)) { - $JSONObj.conditions.users.$condition = @() + $JSONobj.conditions.users.$condition = @() continue } } if ($value -is [array]) { $nonWhitespaceItems = $value | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } if ($nonWhitespaceItems.Count -eq 0) { - $JSONObj.conditions.users.$condition = @() + $JSONobj.conditions.users.$condition = @() continue } } @@ -237,7 +237,7 @@ function New-CIPPCAPolicy { Write-Information "Failed to disable security defaults for tenant $($TenantFilter): $($ErrorMessage.NormalizedError)" } } - $RawJSON = ConvertTo-Json -InputObject $JSONObj -Depth 10 -Compress + $RawJSON = ConvertTo-Json -InputObject $JSONobj -Depth 10 -Compress Write-Information $RawJSON try { Write-Information 'Checking for existing policies' @@ -247,27 +247,31 @@ function New-CIPPCAPolicy { throw "Conditional Access Policy with Display Name $($Displayname) Already exists" return $false } else { + if ($State -eq 'donotchange') { + $JSONobj.state = $CheckExististing.state + $RawJSON = ConvertTo-Json -InputObject $JSONobj -Depth 10 -Compress + } Write-Information "overwriting $($CheckExististing.id)" $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/identity/conditionalAccess/policies/$($CheckExististing.id)" -tenantid $tenantfilter -type PATCH -body $RawJSON -asApp $true - Write-LogMessage -Headers $User -API 'Create CA Policy' -tenant $($Tenant) -message "Updated Conditional Access Policy $($JSONObj.Displayname) to the template standard." -Sev 'Info' + Write-LogMessage -Headers $User -API 'Create CA Policy' -tenant $($Tenant) -message "Updated Conditional Access Policy $($JSONobj.Displayname) to the template standard." -Sev 'Info' return "Updated policy $displayname for $tenantfilter" } } else { Write-Information 'Creating new policy' - if ($JSONobj.GrantControls.authenticationStrength.policyType -or $JSONObj.$jsonobj.LocationInfo) { + if ($JSOObj.GrantControls.authenticationStrength.policyType -or $JSONobj.$JSONobj.LocationInfo) { 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 'Create CA Policy' -tenant $($Tenant) -message "Added Conditional Access Policy $($JSONObj.Displayname)" -Sev 'Info' + Write-LogMessage -Headers $User -API 'Create CA Policy' -tenant $($Tenant) -message "Added Conditional Access Policy $($JSONobj.Displayname)" -Sev 'Info' return "Created policy $displayname for $tenantfilter" } } catch { $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to create or update conditional access rule $($JSONObj.displayName): $($ErrorMessage.NormalizedError) " -sev 'Error' -LogData $ErrorMessage + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to create or update conditional access rule $($JSONobj.displayName): $($ErrorMessage.NormalizedError) " -sev 'Error' -LogData $ErrorMessage - Write-Warning "Failed to create or update conditional access rule $($JSONObj.displayName): $($ErrorMessage.NormalizedError)" + Write-Warning "Failed to create or update conditional access rule $($JSONobj.displayName): $($ErrorMessage.NormalizedError)" Write-Information $_.InvocationInfo.PositionMessage - Write-Information ($JSONObj | ConvertTo-Json -Depth 10) - throw "Failed to create or update conditional access rule $($JSONObj.displayName): $($ErrorMessage.NormalizedError)" + Write-Information ($JSONobj | ConvertTo-Json -Depth 10) + throw "Failed to create or update conditional access rule $($JSONobj.displayName): $($ErrorMessage.NormalizedError)" } } diff --git a/Modules/CIPPCore/Public/New-CIPPGroup.ps1 b/Modules/CIPPCore/Public/New-CIPPGroup.ps1 new file mode 100644 index 000000000000..066f4db19af0 --- /dev/null +++ b/Modules/CIPPCore/Public/New-CIPPGroup.ps1 @@ -0,0 +1,236 @@ +function New-CIPPGroup { + <# + .SYNOPSIS + Creates a new group in Microsoft 365 or Exchange Online + + .DESCRIPTION + Unified function for creating groups that handles all group types consistently. + Used by both direct group creation and group template application. + + .PARAMETER GroupObject + Object containing group properties (displayName, description, groupType, etc.) + + .PARAMETER TenantFilter + The tenant domain name where the group should be created + + .PARAMETER APIName + The API name for logging purposes + + .PARAMETER ExecutingUser + The user executing the request (for logging) + + .EXAMPLE + New-CIPPGroup -GroupObject $GroupData -TenantFilter 'contoso.com' -APIName 'AddGroup' + + .NOTES + Supports all group types: Generic, Security, AzureRole, Dynamic, M365, Distribution, DynamicDistribution + #> + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(Mandatory = $true)] + [PSCustomObject]$GroupObject, + + [Parameter(Mandatory = $true)] + [string]$TenantFilter, + + [Parameter(Mandatory = $false)] + [string]$APIName = 'New-CIPPGroup', + + [Parameter(Mandatory = $false)] + [string]$ExecutingUser = 'CIPP' + ) + + try { + # Normalize group type for consistent handling (accept camelCase from templates) + $NormalizedGroupType = switch -Wildcard ($GroupObject.groupType.ToLower()) { + '*dynamicdistribution*' { 'DynamicDistribution'; break } # Check this first before *dynamic* and *distribution* + '*dynamic*' { 'Dynamic'; break } + '*generic*' { 'Generic'; break } + '*security*' { 'Security'; break } + '*azurerole*' { 'AzureRole'; break } + '*m365*' { 'M365'; break } + '*unified*' { 'M365'; break } + '*microsoft*' { 'M365'; break } + '*distribution*' { 'Distribution'; break } + '*mail*' { 'Distribution'; break } + default { $GroupObject.groupType } + } + + # Determine if this group type needs an email address + $GroupTypesNeedingEmail = @('M365', 'Distribution', 'DynamicDistribution') + $NeedsEmail = $NormalizedGroupType -in $GroupTypesNeedingEmail + + # Determine email address only for group types that need it + $Email = if ($NeedsEmail) { + if ($GroupObject.primDomain.value) { + "$($GroupObject.username)@$($GroupObject.primDomain.value)" + } elseif ($GroupObject.primaryEmailAddress) { + $GroupObject.primaryEmailAddress + } elseif ($GroupObject.username -like '*@*') { + # Username already contains an email address (e.g., from templates with @%tenantfilter%) + $GroupObject.username + } else { + "$($GroupObject.username)@$($TenantFilter)" + } + } else { + $null + } + + Write-LogMessage -API $APIName -tenant $TenantFilter -message "Creating group $($GroupObject.displayName) of type $NormalizedGroupType$(if ($NeedsEmail) { " with email $Email" })" -Sev Info + + # Handle Graph API groups (Security, Generic, AzureRole, Dynamic, M365) + if ($NormalizedGroupType -in @('Generic', 'Security', 'AzureRole', 'Dynamic', 'M365')) { + Write-Information "Creating group $($GroupObject.displayName) of type $NormalizedGroupType$(if ($NeedsEmail) { " with email $Email" })" + $BodyParams = [PSCustomObject]@{ + 'displayName' = $GroupObject.displayName + 'description' = $GroupObject.description + 'mailNickname' = $GroupObject.username + 'mailEnabled' = $false + 'securityEnabled' = $true + 'isAssignableToRole' = ($NormalizedGroupType -eq 'AzureRole') + } + + # Handle dynamic membership + if ($GroupObject.membershipRules) { + $BodyParams | Add-Member -NotePropertyName 'membershipRule' -NotePropertyValue $GroupObject.membershipRules + $BodyParams | Add-Member -NotePropertyName 'membershipRuleProcessingState' -NotePropertyValue 'On' + + if ($NormalizedGroupType -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 for dynamic groups + $SkipStaticMembers = $true + } elseif ($NormalizedGroupType -eq 'M365') { + # Static M365 group + $BodyParams | Add-Member -NotePropertyName 'groupTypes' -NotePropertyValue @('Unified') + $BodyParams.mailEnabled = $true + } + + # Add owners + if ($GroupObject.owners -and $GroupObject.owners.Count -gt 0) { + $OwnerBindings = $GroupObject.owners | ForEach-Object { + if ($_.value) { + "https://graph.microsoft.com/v1.0/users/$($_.value)" + } elseif ($_ -is [string]) { + "https://graph.microsoft.com/v1.0/users/$_" + } + } | Where-Object { $_ } + + if ($OwnerBindings) { + $BodyParams | Add-Member -NotePropertyName 'owners@odata.bind' -NotePropertyValue @($OwnerBindings) + } + } + + # Add members (only for non-dynamic groups) + if ($GroupObject.members -and $GroupObject.members.Count -gt 0 -and -not $SkipStaticMembers) { + $MemberBindings = $GroupObject.members | ForEach-Object { + if ($_.value) { + "https://graph.microsoft.com/v1.0/users/$($_.value)" + } elseif ($_ -is [string]) { + "https://graph.microsoft.com/v1.0/users/$_" + } + } | Where-Object { $_ } + + if ($MemberBindings) { + $BodyParams | Add-Member -NotePropertyName 'members@odata.bind' -NotePropertyValue @($MemberBindings) + } + } + + # Create the group via Graph API + $GraphRequest = New-GraphPostRequest -uri 'https://graph.microsoft.com/beta/groups' -tenantid $TenantFilter -type POST -body (ConvertTo-Json -InputObject $BodyParams -Depth 10) + + $Result = [PSCustomObject]@{ + Success = $true + Message = "Successfully created group $($GroupObject.displayName)" + GroupId = $GraphRequest.id + GroupType = $NormalizedGroupType + Email = if ($NeedsEmail) { $Email } else { $null } + } + + } else { + # Handle Exchange Online groups (Distribution, DynamicDistribution) + + if ($NormalizedGroupType -eq 'DynamicDistribution') { + Write-Information "Creating dynamic distribution group $($GroupObject.displayName) with email $Email" + $ExoParams = @{ + Name = $GroupObject.displayName + RecipientFilter = $GroupObject.membershipRules + PrimarySmtpAddress = $Email + } + $GraphRequest = New-ExoRequest -tenantid $TenantFilter -cmdlet 'New-DynamicDistributionGroup' -cmdParams $ExoParams + + # Set external sender restrictions if specified + if ($null -ne $GroupObject.allowExternal -and $GroupObject.allowExternal -eq $true -and $GraphRequest.Identity) { + $SetParams = @{ + RequireSenderAuthenticationEnabled = [bool]!$GroupObject.allowExternal + Identity = $GraphRequest.Identity + } + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Set-DynamicDistributionGroup' -cmdParams $SetParams + } + + } else { + # Regular Distribution Group + Write-Information "Creating distribution group $($GroupObject.displayName) with email $Email" + + $ExoParams = @{ + Name = $GroupObject.displayName + Alias = $GroupObject.username + Description = $GroupObject.description + PrimarySmtpAddress = $Email + Type = $GroupObject.groupType + RequireSenderAuthenticationEnabled = [bool]!$GroupObject.allowExternal + } + + # Add owners + if ($GroupObject.owners -and $GroupObject.owners.Count -gt 0) { + $OwnerEmails = $GroupObject.owners | ForEach-Object { + if ($_.value) { $_.value } elseif ($_ -is [string]) { $_ } + } | Where-Object { $_ } + + if ($OwnerEmails) { + $ExoParams.ManagedBy = @($OwnerEmails) + } + } + + # Add members + if ($GroupObject.members -and $GroupObject.members.Count -gt 0) { + $MemberEmails = $GroupObject.members | ForEach-Object { + if ($_.value) { $_.value } elseif ($_ -is [string]) { $_ } + } | Where-Object { $_ } + + if ($MemberEmails) { + $ExoParams.Members = @($MemberEmails) + } + } + + $GraphRequest = New-ExoRequest -tenantid $TenantFilter -cmdlet 'New-DistributionGroup' -cmdParams $ExoParams + } + + $Result = [PSCustomObject]@{ + Success = $true + Message = "Successfully created group $($GroupObject.displayName)" + GroupId = $GraphRequest.Identity + GroupType = $NormalizedGroupType + Email = $Email + } + } + + Write-LogMessage -API $APIName -tenant $TenantFilter -message "Created group $($GroupObject.displayName) with id $($Result.GroupId)" -Sev Info + return $Result + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API $APIName -tenant $TenantFilter -message "Group creation failed for $($GroupObject.displayName): $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage + + return [PSCustomObject]@{ + Success = $false + Message = "Failed to create group $($GroupObject.displayName): $($ErrorMessage.NormalizedError)" + Error = $ErrorMessage.NormalizedError + GroupType = $NormalizedGroupType + } + } +} diff --git a/Modules/CIPPCore/Public/Remove-CIPPAutopilotProfile.ps1 b/Modules/CIPPCore/Public/Remove-CIPPAutopilotProfile.ps1 new file mode 100644 index 000000000000..f1a06bbf1e46 --- /dev/null +++ b/Modules/CIPPCore/Public/Remove-CIPPAutopilotProfile.ps1 @@ -0,0 +1,67 @@ +function Remove-CIPPAutopilotProfile { + param( + $ProfileId, + $DisplayName, + $TenantFilter, + $Assignments, + $Headers, + $APIName = 'Remove Autopilot Profile' + ) + + + try { + + try { + $DisplayName = $null -eq $DisplayName ? $ProfileId : $DisplayName + if ($Assignments.Count -gt 0) { + Write-Host "Profile $ProfileId has $($Assignments.Count) assignments, removing them first" + throw + } + + $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles/$ProfileId" -tenantid $TenantFilter -type DELETE + $Result = "Successfully deleted Autopilot profile '$($DisplayName)'" + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev 'Info' + return $Result + } catch { + + # Profile could not be deleted, there is probably an assignment still referencing it. The error is bloody useless here, and we just need to try some stuff + if ($null -eq $Assignments) { + $Assignments = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles/$ProfileId/assignments" -tenantid $TenantFilter + } + + # Remove all assignments + if ($Assignments -and $Assignments.Count -gt 0) { + foreach ($Assignment in $Assignments) { + try { + # Use the assignment ID directly as provided by the API + $AssignmentId = $Assignment.id + $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles/$ProfileId/assignments/$AssignmentId" -tenantid $TenantFilter -type DELETE + + } catch { + # Handle the case where the assignment might reference a deleted group + try { + if ($Assignment.target -and $Assignment.target.'@odata.type' -eq '#microsoft.graph.groupAssignmentTarget') { + $GroupId = $Assignment.target.groupId + $AlternativeAssignmentId = "${ProfileId}_${GroupId}" + $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles/$ProfileId/assignments/$AlternativeAssignmentId" -tenantid $TenantFilter -type DELETE + } + } catch { + throw "Could not remove assignment $AssignmentId" + } + } + } + } + # Retry deleting the profile after removing assignments + $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles/$ProfileId" -tenantid $TenantFilter -type DELETE + $Result = "Successfully deleted Autopilot profile '$($DisplayName)' " + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev 'Info' + return $Result + } + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $ErrorText = "Failed to delete Autopilot profile $ProfileId. Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message $ErrorText -Sev 'Error' -LogData $ErrorMessage + throw $ErrorText + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPContactPermission.ps1 b/Modules/CIPPCore/Public/Set-CIPPContactPermission.ps1 new file mode 100644 index 000000000000..ae61cc3b3458 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPContactPermission.ps1 @@ -0,0 +1,64 @@ +function Set-CIPPContactPermission { + [CmdletBinding(SupportsShouldProcess = $true)] + param( + $APIName = 'Set Contact Permissions', + $Headers, + $RemoveAccess, + $TenantFilter, + $UserID, + $FolderName, + $UserToGetPermissions, + $LoggingName, + $Permissions, + [bool]$SendNotificationToUser = $false + ) + + try { + # If a pretty logging name is not provided, use the ID instead + if ([string]::IsNullOrWhiteSpace($LoggingName) -and $RemoveAccess) { + $LoggingName = $RemoveAccess + } elseif ([string]::IsNullOrWhiteSpace($LoggingName) -and $UserToGetPermissions) { + $LoggingName = $UserToGetPermissions + } + + $ContactParam = [PSCustomObject]@{ + Identity = "$($UserID):\$FolderName" + AccessRights = @($Permissions) + User = $UserToGetPermissions + SendNotificationToUser = $SendNotificationToUser + } + + if ($RemoveAccess) { + if ($PSCmdlet.ShouldProcess("$UserID\$FolderName", "Remove permissions for $LoggingName")) { + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Remove-MailboxFolderPermission' -cmdParams @{Identity = "$($UserID):\$FolderName"; User = $RemoveAccess } + $Result = "Successfully removed access for $LoggingName from contact folder $($ContactParam.Identity)" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -sev Info + } + } else { + if ($PSCmdlet.ShouldProcess("$UserID\$FolderName", "Set permissions for $LoggingName to $Permissions")) { + try { + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Set-MailboxFolderPermission' -cmdParams $ContactParam -Anchor $UserID + } catch { + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Add-MailboxFolderPermission' -cmdParams $ContactParam -Anchor $UserID + } + + $Result = "Successfully set permissions on contact folder $($ContactParam.Identity). The user $LoggingName now has $Permissions permissions on this folder." + + if ($SendNotificationToUser) { + $Result += ' A notification has been sent to the user.' + } + + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -sev Info + } + } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-Warning "Error changing contact permissions $($_.Exception.Message)" + Write-Information $_.InvocationInfo.PositionMessage + $Result = "Failed to set contact permissions for $LoggingName on $UserID : $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -sev Error -LogData $ErrorMessage + throw $Result + } + + return $Result +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDefaultAPDeploymentProfile.ps1 b/Modules/CIPPCore/Public/Set-CIPPDefaultAPDeploymentProfile.ps1 index 502b663199ff..05f1aefffabd 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDefaultAPDeploymentProfile.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDefaultAPDeploymentProfile.ps1 @@ -1,18 +1,18 @@ function Set-CIPPDefaultAPDeploymentProfile { [CmdletBinding(SupportsShouldProcess = $true)] param( - $tenantFilter, - $displayName, - $description, - $devicenameTemplate, - $allowWhiteGlove, + $TenantFilter, + $DisplayName, + $Description, + $DeviceNameTemplate, + $AllowWhiteGlove, $CollectHash, - $userType, + $UserType, $DeploymentMode, - $hideChangeAccount, + $HideChangeAccount, $AssignTo, - $hidePrivacy, - $hideTerms, + $HidePrivacy, + $HideTerms, $AutoKeyboard, $Headers, $Language = 'os-default', @@ -24,65 +24,66 @@ function Set-CIPPDefaultAPDeploymentProfile { try { $ObjBody = [pscustomobject]@{ '@odata.type' = '#microsoft.graph.azureADWindowsAutopilotDeploymentProfile' - 'displayName' = "$($displayName)" - 'description' = "$($description)" - 'deviceNameTemplate' = "$($devicenameTemplate)" + 'displayName' = "$($DisplayName)" + 'description' = "$($Description)" + 'deviceNameTemplate' = "$($DeviceNameTemplate)" 'language' = "$($Language)" - 'enableWhiteGlove' = $([bool]($allowWhiteGlove)) + 'enableWhiteGlove' = $([bool]($AllowWhiteGlove)) 'deviceType' = 'windowsPc' 'extractHardwareHash' = $([bool]($CollectHash)) 'roleScopeTagIds' = @() 'hybridAzureADJoinSkipConnectivityCheck' = $false - 'outOfBoxExperienceSetting' = @{ - 'deviceUsageType' = "$DeploymentMode" - 'escapeLinkHidden' = $([bool]($hideChangeAccount)) - 'privacySettingsHidden' = $([bool]($hidePrivacy)) - 'eulaHidden' = $([bool]($hideTerms)) - 'userType' = "$userType" + 'outOfBoxExperienceSetting' = @{ + 'deviceUsageType' = "$DeploymentMode" + 'escapeLinkHidden' = $([bool]($HideChangeAccount)) + 'privacySettingsHidden' = $([bool]($HidePrivacy)) + 'eulaHidden' = $([bool]($HideTerms)) + 'userType' = "$UserType" 'keyboardSelectionPageSkipped' = $([bool]($AutoKeyboard)) } } $Body = ConvertTo-Json -InputObject $ObjBody - $Profiles = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles' -tenantid $tenantFilter | Where-Object -Property displayName -EQ $displayName + $Profiles = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles' -tenantid $TenantFilter | Where-Object -Property displayName -EQ $DisplayName if ($Profiles.count -gt 1) { $Profiles | ForEach-Object { if ($_.id -ne $Profiles[0].id) { if ($PSCmdlet.ShouldProcess($_.displayName, 'Delete duplicate Autopilot profile')) { - $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles/$($_.id)" -tenantid $tenantFilter -type DELETE - Write-LogMessage -Headers $User -API $APIName -tenant $($tenantFilter) -message "Deleted duplicate Autopilot profile $($displayName)" -Sev 'Info' + $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles/$($_.id)" -tenantid $TenantFilter -type DELETE + Write-LogMessage -Headers $User -API $APIName -tenant $($TenantFilter) -message "Deleted duplicate Autopilot profile $($DisplayName)" -Sev 'Info' } } } $Profiles = $Profiles[0] } if (!$Profiles) { - if ($PSCmdlet.ShouldProcess($displayName, 'Add Autopilot profile')) { + if ($PSCmdlet.ShouldProcess($DisplayName, 'Add Autopilot profile')) { $Type = 'Add' - $GraphRequest = New-GraphPostRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles' -body $body -tenantid $tenantFilter - Write-LogMessage -Headers $User -API $APIName -tenant $($tenantFilter) -message "Added Autopilot profile $($displayName)" -Sev 'Info' + $GraphRequest = New-GraphPostRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles' -body $Body -tenantid $TenantFilter + Write-LogMessage -Headers $User -API $APIName -tenant $($TenantFilter) -message "Added Autopilot profile $($DisplayName)" -Sev 'Info' } } else { $Type = 'Edit' - $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles/$($Profiles.id)" -tenantid $tenantFilter -body $body -type PATCH + $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles/$($Profiles.id)" -tenantid $TenantFilter -body $Body -type PATCH $GraphRequest = $Profiles | Select-Object -Last 1 } if ($AssignTo -eq $true) { $AssignBody = '{"target":{"@odata.type":"#microsoft.graph.allDevicesAssignmentTarget"}}' - if ($PSCmdlet.ShouldProcess($AssignTo, "Assign Autopilot profile $displayName")) { + if ($PSCmdlet.ShouldProcess($AssignTo, "Assign Autopilot profile $DisplayName")) { #Get assignments - $Assignments = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles/$($GraphRequest.id)/assignments" -tenantid $tenantFilter + $Assignments = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles/$($GraphRequest.id)/assignments" -tenantid $TenantFilter if (!$Assignments) { - $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles/$($GraphRequest.id)/assignments" -tenantid $tenantFilter -type POST -body $AssignBody + $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles/$($GraphRequest.id)/assignments" -tenantid $TenantFilter -type POST -body $AssignBody } - Write-LogMessage -Headers $User -API $APIName -tenant $tenantFilter -message "Assigned autopilot profile $($displayName) to $AssignTo" -Sev 'Info' + Write-LogMessage -Headers $User -API $APIName -tenant $TenantFilter -message "Assigned autopilot profile $($DisplayName) to $AssignTo" -Sev 'Info' } } - "Successfully $($Type)ed profile for $tenantFilter" + "Successfully $($Type)ed profile for $TenantFilter" } catch { $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -Headers $User -API $APIName -tenant $tenantFilter -message "Failed $($Type)ing Autopilot Profile $($displayName). Error: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage - throw "Failed to add profile for $($tenantFilter): $($ErrorMessage.NormalizedError)" + $Result = "Failed $($Type)ing Autopilot Profile $($DisplayName). Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -Headers $User -API $APIName -tenant $TenantFilter -message $Result -Sev 'Error' -LogData $ErrorMessage + throw $Result } } diff --git a/Modules/CIPPCore/Public/Set-CIPPDefaultAPEnrollment.ps1 b/Modules/CIPPCore/Public/Set-CIPPDefaultAPEnrollment.ps1 index d02794c16c0a..171e1d732695 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDefaultAPEnrollment.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDefaultAPEnrollment.ps1 @@ -8,14 +8,13 @@ function Set-CIPPDefaultAPEnrollment { $EnableLog, $ErrorMessage, $TimeOutInMinutes, + $InstallWindowsUpdates, $AllowFail, $OBEEOnly, $Headers, $APIName = 'Add Default Enrollment Status Page' ) - $User = $Request.Headers - try { $ObjBody = [pscustomobject]@{ '@odata.type' = '#microsoft.graph.windows10EnrollmentCompletionPageConfiguration' @@ -28,6 +27,7 @@ function Set-CIPPDefaultAPEnrollment { 'allowLogCollectionOnInstallFailure' = [bool]$EnableLog 'customErrorMessage' = "$ErrorMessage" 'installProgressTimeoutInMinutes' = $TimeOutInMinutes + 'installQualityUpdates' = [bool]$InstallWindowsUpdates 'allowDeviceUseOnInstallFailure' = [bool]$AllowFail 'selectedMobileAppIds' = @() 'trackInstallProgressForAutopilotOnly' = [bool]$OBEEOnly @@ -40,11 +40,11 @@ function Set-CIPPDefaultAPEnrollment { if ($PSCmdlet.ShouldProcess($ExistingStatusPage.ID, 'Set Default Enrollment Status Page')) { $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/deviceEnrollmentConfigurations/$($ExistingStatusPage.ID)" -body $Body -Type PATCH -tenantid $TenantFilter "Successfully changed default enrollment status page for $TenantFilter" - Write-LogMessage -Headers $User -API $APIName -tenant $TenantFilter -message "Added Autopilot Enrollment Status Page $($ExistingStatusPage.displayName)" -Sev 'Info' + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message "Added Autopilot Enrollment Status Page $($ExistingStatusPage.displayName)" -Sev 'Info' } } catch { $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -Headers $User -API $APIName -tenant $TenantFilter -message "Failed adding Autopilot Enrollment Status Page $($ExistingStatusPage.displayName). Error: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message "Failed adding Autopilot Enrollment Status Page $($ExistingStatusPage.displayName). Error: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage throw "Failed to change default enrollment status page for $($TenantFilter): $($ErrorMessage.NormalizedError)" } } diff --git a/Modules/CIPPCore/Public/Set-CIPPOutOfoffice.ps1 b/Modules/CIPPCore/Public/Set-CIPPOutOfoffice.ps1 index ced3fad0a5af..19f7fae9e445 100644 --- a/Modules/CIPPCore/Public/Set-CIPPOutOfoffice.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPOutOfoffice.ps1 @@ -18,8 +18,9 @@ function Set-CIPPOutOfOffice { try { $CmdParams = @{ - Identity = $UserID - AutoReplyState = $State + Identity = $UserID + AutoReplyState = $State + ExternalAudience = 'None' } if ($PSBoundParameters.ContainsKey('InternalMessage')) { @@ -28,6 +29,7 @@ function Set-CIPPOutOfOffice { if ($PSBoundParameters.ContainsKey('ExternalMessage')) { $CmdParams.ExternalMessage = $ExternalMessage + $CmdParams.ExternalAudience = 'All' } if ($State -eq 'Scheduled') { diff --git a/Modules/CIPPCore/Public/Standards/Get-CIPPStandards.ps1 b/Modules/CIPPCore/Public/Standards/Get-CIPPStandards.ps1 index b639cf088d80..9ad77e61923d 100644 --- a/Modules/CIPPCore/Public/Standards/Get-CIPPStandards.ps1 +++ b/Modules/CIPPCore/Public/Standards/Get-CIPPStandards.ps1 @@ -31,6 +31,72 @@ function Get-CIPPStandards { $_.GUID -like $TemplateId -and $_.runManually -eq $runManually } + # 1.5. Expand templates that contain TemplateList-Tags into multiple standards + $ExpandedTemplates = foreach ($Template in $Templates) { + $NewTemplate = $Template.PSObject.Copy() + $ExpandedStandards = [ordered]@{} + $HasExpansions = $false + + foreach ($StandardName in $Template.standards.PSObject.Properties.Name) { + $StandardValue = $Template.standards.$StandardName + $IsArray = $StandardValue -is [System.Collections.IEnumerable] -and -not ($StandardValue -is [string]) + + if ($IsArray) { + $NewArray = @() + foreach ($Item in $StandardValue) { + if ($Item.'TemplateList-Tags'.value) { + $HasExpansions = $true + $Table = Get-CippTable -tablename 'templates' + $Filter = "PartitionKey eq 'IntuneTemplate'" + $TemplatesList = Get-CIPPAzDataTableEntity @Table -Filter $Filter | Where-Object -Property package -EQ $Item.'TemplateList-Tags'.value + + foreach ($TemplateItem in $TemplatesList) { + $NewItem = $Item.PSObject.Copy() + $NewItem.PSObject.Properties.Remove('TemplateList-Tags') + $NewItem | Add-Member -NotePropertyName TemplateList -NotePropertyValue ([pscustomobject]@{ + label = "$($TemplateItem.RowKey)" + value = "$($TemplateItem.RowKey)" + }) -Force + $NewArray = $NewArray + $NewItem + } + } else { + $NewArray = $NewArray + $Item + } + } + $ExpandedStandards[$StandardName] = $NewArray + } else { + if ($StandardValue.'TemplateList-Tags'.value) { + $HasExpansions = $true + $Table = Get-CippTable -tablename 'templates' + $Filter = "PartitionKey eq 'IntuneTemplate'" + $TemplatesList = Get-CIPPAzDataTableEntity @Table -Filter $Filter | Where-Object -Property package -EQ $StandardValue.'TemplateList-Tags'.value + + $NewArray = @() + foreach ($TemplateItem in $TemplatesList) { + $NewItem = $StandardValue.PSObject.Copy() + $NewItem.PSObject.Properties.Remove('TemplateList-Tags') + $NewItem | Add-Member -NotePropertyName TemplateList -NotePropertyValue ([pscustomobject]@{ + label = "$($TemplateItem.RowKey)" + value = "$($TemplateItem.RowKey)" + }) -Force + $NewArray = $NewArray + $NewItem + } + $ExpandedStandards[$StandardName] = $NewArray + } else { + $ExpandedStandards[$StandardName] = $StandardValue + } + } + } + + if ($HasExpansions) { + $NewTemplate.standards = [pscustomobject]$ExpandedStandards + } + + $NewTemplate + } + + $Templates = $ExpandedTemplates + # 2. Get tenant list, filter if needed $AllTenantsList = Get-Tenants if ($TenantFilter -ne 'allTenants') { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDKIM.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDKIM.ps1 index 49e881023c41..f03d6d1f35d6 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDKIM.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDKIM.ps1 @@ -32,7 +32,7 @@ function Invoke-CIPPStandardAddDKIM { param($Tenant, $Settings) #$Rerun -Type Standard -Tenant $Tenant -API 'AddDKIM' -Settings $Settings - $TestResult = Test-CIPPStandardLicense -StandardName 'AddDKIM' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'AddDKIM' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiPhishPolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiPhishPolicy.ps1 index 5952765a17b5..0ea8bfd4f03a 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiPhishPolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiPhishPolicy.ps1 @@ -50,7 +50,7 @@ function Invoke-CIPPStandardAntiPhishPolicy { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'AntiPhishPolicy' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'AntiPhishPolicy' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiSpamSafeList.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiSpamSafeList.ps1 index 4b2750be7fa0..855c2685624b 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiSpamSafeList.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiSpamSafeList.ps1 @@ -29,7 +29,7 @@ function Invoke-CIPPStandardAntiSpamSafeList { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'AntiSpamSafeList' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'AntiSpamSafeList' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." @@ -41,7 +41,7 @@ function Invoke-CIPPStandardAntiSpamSafeList { $State = [System.Convert]::ToBoolean($Settings.EnableSafeList) } catch { Write-LogMessage -API 'Standards' -tenant $Tenant -message 'AntiSpamSafeList: Failed to convert the EnableSafeList parameter to a boolean' -sev Error - Return + return } try { @@ -49,7 +49,7 @@ function Invoke-CIPPStandardAntiSpamSafeList { } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to get the Anti-Spam Connection Filter Safe List. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Return + return } $WantedState = $State -eq $true ? $true : $false $StateIsCorrect = if ($CurrentState -eq $WantedState) { $true } else { $false } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAuditLog.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAuditLog.ps1 index 81ae65729879..79e6269c7d97 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAuditLog.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAuditLog.ps1 @@ -32,7 +32,7 @@ function Invoke-CIPPStandardAuditLog { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'AuditLog' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'AuditLog' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." @@ -43,7 +43,7 @@ function Invoke-CIPPStandardAuditLog { Write-Host ($Settings | ConvertTo-Json) $AuditLogEnabled = [bool](New-ExoRequest -tenantid $Tenant -cmdlet 'Get-AdminAuditLogConfig' -Select UnifiedAuditLogIngestionEnabled).UnifiedAuditLogIngestionEnabled - If ($Settings.remediate -eq $true) { + if ($Settings.remediate -eq $true) { Write-Host 'Time to remediate' $DehydratedTenant = (New-ExoRequest -tenantid $Tenant -cmdlet 'Get-OrganizationConfig' -Select IsDehydrated).IsDehydrated diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoExpandArchive.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoExpandArchive.ps1 index c0ee4bc5bdf3..049f9a8a1dc8 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoExpandArchive.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoExpandArchive.ps1 @@ -28,7 +28,7 @@ function Invoke-CIPPStandardAutoExpandArchive { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'AutoExpandArchive' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'AutoExpandArchive' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotStatusPage.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotStatusPage.ps1 index 97851eefeeb0..33038ccdc207 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotStatusPage.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotStatusPage.ps1 @@ -15,14 +15,16 @@ function Invoke-CIPPStandardAutopilotStatusPage { TAG DISABLEDFEATURES {"report":false,"warn":false,"remediate":false} + EXECUTIVETEXT + Provides employees with a visual progress indicator during automated device setup, improving the user experience when receiving new computers. This reduces IT support calls and helps ensure successful device deployment by guiding users through the setup process. ADDEDCOMPONENT {"type":"number","name":"standards.AutopilotStatusPage.TimeOutInMinutes","label":"Timeout in minutes","defaultValue":60} {"type":"textField","name":"standards.AutopilotStatusPage.ErrorMessage","label":"Custom Error Message","required":false} {"type":"switch","name":"standards.AutopilotStatusPage.ShowProgress","label":"Show progress to users","defaultValue":true} {"type":"switch","name":"standards.AutopilotStatusPage.EnableLog","label":"Turn on log collection","defaultValue":true} {"type":"switch","name":"standards.AutopilotStatusPage.OBEEOnly","label":"Show status page only with OOBE setup","defaultValue":true} + {"type":"switch","name":"standards.AutopilotStatusPage.InstallWindowsUpdates","label":"Install Windows Updates during setup","defaultValue":true} {"type":"switch","name":"standards.AutopilotStatusPage.BlockDevice","label":"Block device usage during setup","defaultValue":true} - {"type":"switch","name":"standards.AutopilotStatusPage.AllowRetry","label":"Allow retry","defaultValue":true} {"type":"switch","name":"standards.AutopilotStatusPage.AllowReset","label":"Allow reset","defaultValue":true} {"type":"switch","name":"standards.AutopilotStatusPage.AllowFail","label":"Allow users to use device if setup fails","defaultValue":true} IMPACT @@ -46,7 +48,10 @@ function Invoke-CIPPStandardAutopilotStatusPage { } #we're done. try { $CurrentConfig = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/deviceEnrollmentConfigurations?`$expand=assignments&orderBy=priority&`$filter=deviceEnrollmentConfigurationType eq 'windows10EnrollmentCompletionPageConfiguration' and priority eq 0" -tenantid $Tenant | - Select-Object -Property id, displayName, priority, showInstallationProgress, blockDeviceSetupRetryByUser, allowDeviceResetOnInstallFailure, allowLogCollectionOnInstallFailure, customErrorMessage, installProgressTimeoutInMinutes, allowDeviceUseOnInstallFailure, trackInstallProgressForAutopilotOnly + Select-Object -Property id, displayName, priority, showInstallationProgress, blockDeviceSetupRetryByUser, allowDeviceResetOnInstallFailure, allowLogCollectionOnInstallFailure, customErrorMessage, installProgressTimeoutInMinutes, allowDeviceUseOnInstallFailure, trackInstallProgressForAutopilotOnly, installQualityUpdates + + # Compatibility for standards made in v8.3.0 or before, which did not have the InstallWindowsUpdates setting + $InstallWindowsUpdates = $Settings.InstallWindowsUpdates ?? $false $StateIsCorrect = ($CurrentConfig.installProgressTimeoutInMinutes -eq $Settings.TimeOutInMinutes) -and ($CurrentConfig.customErrorMessage -eq $Settings.ErrorMessage) -and @@ -54,6 +59,7 @@ function Invoke-CIPPStandardAutopilotStatusPage { ($CurrentConfig.allowLogCollectionOnInstallFailure -eq $Settings.EnableLog) -and ($CurrentConfig.trackInstallProgressForAutopilotOnly -eq $Settings.OBEEOnly) -and ($CurrentConfig.blockDeviceSetupRetryByUser -eq !$Settings.BlockDevice) -and + ($CurrentConfig.installQualityUpdates -eq $InstallWindowsUpdates) -and ($CurrentConfig.allowDeviceResetOnInstallFailure -eq $Settings.AllowReset) -and ($CurrentConfig.allowDeviceUseOnInstallFailure -eq $Settings.AllowFail) } catch { @@ -66,15 +72,16 @@ function Invoke-CIPPStandardAutopilotStatusPage { if ($Settings.remediate -eq $true) { try { $Parameters = @{ - TenantFilter = $Tenant - ShowProgress = $Settings.ShowProgress - BlockDevice = $Settings.BlockDevice - AllowReset = $Settings.AllowReset - EnableLog = $Settings.EnableLog - ErrorMessage = $Settings.ErrorMessage - TimeOutInMinutes = $Settings.TimeOutInMinutes - AllowFail = $Settings.AllowFail - OBEEOnly = $Settings.OBEEOnly + TenantFilter = $Tenant + ShowProgress = $Settings.ShowProgress + BlockDevice = $Settings.BlockDevice + InstallWindowsUpdates = $InstallWindowsUpdates + AllowReset = $Settings.AllowReset + EnableLog = $Settings.EnableLog + ErrorMessage = $Settings.ErrorMessage + TimeOutInMinutes = $Settings.TimeOutInMinutes + AllowFail = $Settings.AllowFail + OBEEOnly = $Settings.OBEEOnly } Set-CIPPDefaultAPEnrollment @Parameters diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardBookings.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardBookings.ps1 index 6c8f529b2de0..f1a4b6727ccf 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardBookings.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardBookings.ps1 @@ -29,7 +29,7 @@ function Invoke-CIPPStandardBookings { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'Bookings' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'Bookings' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardCloudMessageRecall.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardCloudMessageRecall.ps1 index c7ff49f9cc94..97a8bd07448a 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardCloudMessageRecall.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardCloudMessageRecall.ps1 @@ -29,7 +29,7 @@ function Invoke-CIPPStandardCloudMessageRecall { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'CloudMessageRecall' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'CloudMessageRecall' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDelegateSentItems.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDelegateSentItems.ps1 index e3686fec0076..561f2919db2e 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDelegateSentItems.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDelegateSentItems.ps1 @@ -29,7 +29,7 @@ function Invoke-CIPPStandardDelegateSentItems { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'DelegateSentItems' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'DelegateSentItems' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 index 63f77574f974..a37a37b6190c 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 @@ -32,7 +32,7 @@ function Invoke-CIPPStandardDeployContactTemplates { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'DeployContactTemplates' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'DeployContactTemplates' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1 index aa419c59f095..6f7798fd86b6 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1 @@ -33,7 +33,7 @@ function Invoke-CIPPStandardDeployMailContact { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'DeployMailContact' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'DeployMailContact' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAdditionalStorageProviders.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAdditionalStorageProviders.ps1 index f0275a9eb92c..1ebc05b7c365 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAdditionalStorageProviders.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAdditionalStorageProviders.ps1 @@ -31,7 +31,7 @@ function Invoke-CIPPStandardDisableAdditionalStorageProviders { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'DisableAdditionalStorageProviders' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'DisableAdditionalStorageProviders' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 index 37a64f99a319..72585d50ec4d 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 @@ -30,7 +30,7 @@ function Invoke-CIPPStandardDisableBasicAuthSMTP { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'DisableBasicAuthSMTP' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'DisableBasicAuthSMTP' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExchangeOnlinePowerShell.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExchangeOnlinePowerShell.ps1 index 82d25184ccaf..a83b5033a821 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExchangeOnlinePowerShell.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExchangeOnlinePowerShell.ps1 @@ -33,7 +33,7 @@ function Invoke-CIPPStandardDisableExchangeOnlinePowerShell { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'DisableExchangeOnlinePowerShell' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'DisableExchangeOnlinePowerShell' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExternalCalendarSharing.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExternalCalendarSharing.ps1 index 6b4c29c2be3b..6e8cb00c8a49 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExternalCalendarSharing.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExternalCalendarSharing.ps1 @@ -31,7 +31,7 @@ function Invoke-CIPPStandardDisableExternalCalendarSharing { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'DisableExternalCalendarSharing' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'DisableExternalCalendarSharing' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableOutlookAddins.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableOutlookAddins.ps1 index 11e3db4739cf..047374531541 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableOutlookAddins.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableOutlookAddins.ps1 @@ -31,7 +31,7 @@ function Invoke-CIPPStandardDisableOutlookAddins { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'DisableOutlookAddins' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'DisableOutlookAddins' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 index 4f284d19857f..fcfaa7653f87 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 @@ -31,7 +31,7 @@ function Invoke-CIPPStandardDisableResourceMailbox { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'DisableResourceMailbox' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'DisableResourceMailbox' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTNEF.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTNEF.ps1 index e6bd16f501ba..ce70d89078e1 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTNEF.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTNEF.ps1 @@ -30,7 +30,7 @@ function Invoke-CIPPStandardDisableTNEF { param ($Tenant, $Settings) ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'DisableTNEF' - $TestResult = Test-CIPPStandardLicense -StandardName 'DisableTNEF' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'DisableTNEF' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEXODisableAutoForwarding.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEXODisableAutoForwarding.ps1 index b98f28fe2fd3..8d4011a60bd8 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEXODisableAutoForwarding.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEXODisableAutoForwarding.ps1 @@ -33,7 +33,7 @@ function Invoke-CIPPStandardEXODisableAutoForwarding { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'EXODisableAutoForwarding' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'EXODisableAutoForwarding' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEXOOutboundSpamLimits.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEXOOutboundSpamLimits.ps1 index d29e2af9634f..67dab06c3bdd 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEXOOutboundSpamLimits.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEXOOutboundSpamLimits.ps1 @@ -7,7 +7,7 @@ function Invoke-CIPPStandardEXOOutboundSpamLimits { .SYNOPSIS (Label) Set Exchange Outbound Spam Limits .DESCRIPTION - (Helptext) Configures the outbound spam recipient limits (external per hour, internal per hour, per day) and the action to take when a limit is reached. The 'Set Outbound Spam Alert e-mail' standard is recommended to configure together with this one. + (Helptext) Configures the outbound spam recipient limits (external per hour, internal per hour, per day) and the action to take when a limit is reached. The 'Set Outbound Spam Alert e-mail' standard is recommended to configure together with this one. (DocsDescription) Configures the Exchange Online outbound spam recipient limits for external per hour, internal per hour, and per day, along with the action to take (e.g., BlockUser, Alert) when these limits are exceeded. This helps prevent abuse and manage email flow. Microsoft's recommendations can be found [here.](https://learn.microsoft.com/en-us/defender-office-365/recommended-settings-for-eop-and-office365#eop-outbound-spam-policy-settings) The 'Set Outbound Spam Alert e-mail' standard is recommended to configure together with this one. .NOTES CAT @@ -35,7 +35,7 @@ function Invoke-CIPPStandardEXOOutboundSpamLimits { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'EXOOutboundSpamLimits' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'EXOOutboundSpamLimits' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableLitigationHold.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableLitigationHold.ps1 index 1e1df95f041d..f959049e9ffa 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableLitigationHold.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableLitigationHold.ps1 @@ -29,7 +29,7 @@ function Invoke-CIPPStandardEnableLitigationHold { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'EnableLitigationHold' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'EnableLitigationHold' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailTips.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailTips.ps1 index 4e251944fc17..e1a58fd11da9 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailTips.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailTips.ps1 @@ -33,7 +33,7 @@ function Invoke-CIPPStandardEnableMailTips { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'EnableMailTips' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'EnableMailTips' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailboxAuditing.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailboxAuditing.ps1 index 2676d7577a04..cf538a561585 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailboxAuditing.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailboxAuditing.ps1 @@ -32,7 +32,7 @@ function Invoke-CIPPStandardEnableMailboxAuditing { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'EnableMailboxAuditing' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'EnableMailboxAuditing' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableOnlineArchiving.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableOnlineArchiving.ps1 index 6710fce40846..b98ea70abcc5 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableOnlineArchiving.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableOnlineArchiving.ps1 @@ -28,7 +28,7 @@ function Invoke-CIPPStandardEnableOnlineArchiving { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'EnableOnlineArchiving' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'EnableOnlineArchiving' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExchangeConnectorTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExchangeConnectorTemplate.ps1 index b57860ecf55b..ca28aaed9122 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExchangeConnectorTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExchangeConnectorTemplate.ps1 @@ -4,7 +4,7 @@ function Invoke-CIPPStandardExchangeConnectorTemplate { Internal #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'ExConnector' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'ExConnector' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardFocusedInbox.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardFocusedInbox.ps1 index 268a521c020c..93a2d324ddd7 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardFocusedInbox.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardFocusedInbox.ps1 @@ -29,7 +29,7 @@ function Invoke-CIPPStandardFocusedInbox { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'FocusedInbox' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'FocusedInbox' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGlobalQuarantineNotifications.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGlobalQuarantineNotifications.ps1 index ea0284c0e2ba..1eff1e59ac2f 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGlobalQuarantineNotifications.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGlobalQuarantineNotifications.ps1 @@ -30,7 +30,7 @@ function Invoke-CIPPStandardGlobalQuarantineNotifications { param ($Tenant, $Settings) ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'GlobalQuarantineNotifications' - $TestResult = Test-CIPPStandardLicense -StandardName 'GlobalQuarantineNotifications' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'GlobalQuarantineNotifications' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 index 29c84354828e..715482c0fc80 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 @@ -28,90 +28,194 @@ function Invoke-CIPPStandardGroupTemplate { https://docs.cipp.app/user-documentation/tenant/standards/list-standards #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'GroupTemplate' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access - if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." - return $true - } #we're done. ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'GroupTemplate' $existingGroups = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/groups?$top=999' -tenantid $tenant + + $TestResult = Test-CIPPStandardLicense -StandardName 'GroupTemplate' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') -SkipLog + + $Settings.groupTemplate ? ($Settings | Add-Member -NotePropertyName 'TemplateList' -NotePropertyValue $Settings.groupTemplate) : $null + + $Table = Get-CippTable -tablename 'templates' + $Filter = "PartitionKey eq 'GroupTemplate' and (RowKey eq '$($Settings.TemplateList.value -join "' or RowKey eq '")')" + $GroupTemplates = (Get-CIPPAzDataTableEntity @Table -Filter $Filter).JSON | ConvertFrom-Json + + if ('dynamicDistribution' -in $GroupTemplates.groupType) { + # Get dynamic distro list from exchange + $DynamicDistros = New-ExoRequest -cmdlet 'Get-DynamicDistributionGroup' -tenantid $tenant -Select 'Identity,Name,Alias,RecipientFilter,PrimarySmtpAddress' + } + if ($Settings.remediate -eq $true) { #Because the list name changed from TemplateList to groupTemplate by someone :@, we'll need to set it back to TemplateList - $Settings.groupTemplate ? ($Settings | Add-Member -NotePropertyName 'TemplateList' -NotePropertyValue $Settings.groupTemplate) : $null + Write-Host "Settings: $($Settings.TemplateList | ConvertTo-Json)" - foreach ($Template in $Settings.TemplateList) { + foreach ($Template in $GroupTemplates) { + Write-Information "Processing template: $($Template.displayName)" try { - $Table = Get-CippTable -tablename 'templates' - $Filter = "PartitionKey eq 'GroupTemplate' and RowKey eq '$($Template.value)'" - $groupobj = (Get-AzDataTableEntity @Table -Filter $Filter).JSON | ConvertFrom-Json - $email = if ($groupobj.domain) { "$($groupobj.username)@$($groupobj.domain)" } else { "$($groupobj.username)@$($Tenant)" } - $CheckExististing = $existingGroups | Where-Object -Property displayName -EQ $groupobj.displayname - $BodyToship = [pscustomobject] @{ - 'displayName' = $groupobj.Displayname - 'description' = $groupobj.Description - 'mailNickname' = $groupobj.username - mailEnabled = [bool]$false - securityEnabled = [bool]$true - } - if ($groupobj.groupType -eq 'AzureRole') { - $BodyToship | Add-Member -NotePropertyName 'isAssignableToRole' -NotePropertyValue $true - } - if ($groupobj.membershipRules) { - $BodyToship | Add-Member -NotePropertyName 'membershipRule' -NotePropertyValue ($groupobj.membershipRules) - $BodyToship | Add-Member -NotePropertyName 'groupTypes' -NotePropertyValue @('DynamicMembership') - $BodyToship | Add-Member -NotePropertyName 'membershipRuleProcessingState' -NotePropertyValue 'On' + $groupobj = $Template + + if ($Template.groupType -eq 'dynamicDistribution') { + $CheckExisting = $DynamicDistros | Where-Object { $_.Name -eq $Template.displayName } + } else { + $CheckExisting = $existingGroups | Where-Object -Property displayName -EQ $groupobj.displayName } - if (!$CheckExististing) { + + if (!$CheckExisting) { + Write-Information 'Creating group' $ActionType = 'create' - if ($groupobj.groupType -in 'Generic', 'azurerole', 'dynamic', 'Security') { - $GraphRequest = New-GraphPostRequest -uri 'https://graph.microsoft.com/beta/groups' -tenantid $tenant -type POST -body (ConvertTo-Json -InputObject $BodyToship -Depth 10) -verbose - } else { - if ($groupobj.groupType -eq 'dynamicdistribution') { - $Params = @{ - Name = $groupobj.Displayname - RecipientFilter = $groupobj.membershipRules - PrimarySmtpAddress = $email - } - $GraphRequest = New-ExoRequest -tenantid $tenant -cmdlet 'New-DynamicDistributionGroup' -cmdParams $params - } else { - $Params = @{ - Name = $groupobj.Displayname - Alias = $groupobj.username - Description = $groupobj.Description - PrimarySmtpAddress = $email - Type = $groupobj.groupType - RequireSenderAuthenticationEnabled = [bool]!$groupobj.AllowExternal - } - $GraphRequest = New-ExoRequest -tenantid $tenant -cmdlet 'New-DistributionGroup' -cmdParams $params - } + + # Check if Exchange license is required for distribution groups + if ($groupobj.groupType -in @('distribution', 'dynamicdistribution') -and !$TestResult) { + Write-LogMessage -API 'Standards' -tenant $tenant -message "Cannot create group $($groupobj.displayname) as the tenant is not licensed for Exchange." -Sev 'Error' + continue + } + + # Use the centralized New-CIPPGroup function + $Result = New-CIPPGroup -GroupObject $groupobj -TenantFilter $tenant -APIName 'Standards' -ExecutingUser 'CIPP-Standards' + + if (!$Result.Success) { + Write-Information "Failed to create group $($groupobj.displayname): $($Result.Message)" + continue } - Write-LogMessage -API 'Standards' -tenant $tenant -message "Created group $($groupobj.displayname) with id $($GraphRequest.id) " -Sev 'Info' } else { $ActionType = 'update' - if ($groupobj.groupType -in 'Generic', 'azurerole', 'dynamic') { - $GraphRequest = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/groups/$($CheckExististing.id)" -tenantid $tenant -type PATCH -body (ConvertTo-Json -InputObject $BodyToship -Depth 10) -verbose + + # Normalize group type like New-CIPPGroup does + $NormalizedGroupType = switch -Wildcard ($groupobj.groupType.ToLower()) { + '*dynamicdistribution*' { 'DynamicDistribution'; break } + '*dynamic*' { 'Dynamic'; break } + '*generic*' { 'Generic'; break } + '*security*' { 'Security'; break } + '*azurerole*' { 'AzureRole'; break } + '*m365*' { 'M365'; break } + '*unified*' { 'M365'; break } + '*microsoft*' { 'M365'; break } + '*distribution*' { 'Distribution'; break } + '*mail*' { 'Distribution'; break } + default { $groupobj.groupType } + } + + # Handle Graph API groups (Security, Generic, AzureRole, Dynamic, M365) + if ($NormalizedGroupType -in @('Generic', 'Security', 'AzureRole', 'Dynamic', 'M365')) { + + # Compare existing group with template to determine what needs updating + $PatchBody = [PSCustomObject]@{} + $ChangesNeeded = [System.Collections.Generic.List[string]]::new() + + # Check description + if ($CheckExisting.description -ne $groupobj.description) { + $PatchBody | Add-Member -NotePropertyName 'description' -NotePropertyValue $groupobj.description + $ChangesNeeded.Add("description: '$($CheckExisting.description)' → '$($groupobj.description)'") + } + + # Handle membership rules for dynamic groups + # Only update if the template specifies this should be a dynamic group + if ($NormalizedGroupType -eq 'Dynamic' -and $groupobj.membershipRules) { + if ($CheckExisting.membershipRule -ne $groupobj.membershipRules) { + $PatchBody | Add-Member -NotePropertyName 'membershipRule' -NotePropertyValue $groupobj.membershipRules + $PatchBody | Add-Member -NotePropertyName 'membershipRuleProcessingState' -NotePropertyValue 'On' + $ChangesNeeded.Add("membershipRule: '$($CheckExisting.membershipRule)' → '$($groupobj.membershipRules)'") + } + } + + # Only patch if there are actual changes + if ($ChangesNeeded.Count -gt 0) { + $GraphRequest = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/groups/$($CheckExisting.id)" -tenantid $tenant -type PATCH -body (ConvertTo-Json -InputObject $PatchBody -Depth 10) + Write-LogMessage -API 'Standards' -tenant $tenant -message "Updated Group '$($groupobj.displayName)' - Changes: $($ChangesNeeded -join ', ')" -Sev Info + } else { + Write-Information "Group '$($groupobj.displayName)' already matches template - no update needed" + } + } else { - if ($groupobj.groupType -eq 'dynamicdistribution') { - $Params = @{ - Name = $groupobj.Displayname - RecipientFilter = $groupobj.membershipRules - PrimarySmtpAddress = $email + # Handle Exchange Online groups (Distribution, DynamicDistribution) + if (!$TestResult) { + Write-LogMessage -API 'Standards' -tenant $tenant -message "Cannot update group $($groupobj.displayName) as the tenant is not licensed for Exchange." -Sev 'Error' + continue + } + + # Construct email address if needed + $Email = if ($groupobj.username -like '*@*') { + $groupobj.username + } else { + "$($groupobj.username)@$($tenant)" + } + + $ExoChangesNeeded = [System.Collections.Generic.List[string]]::new() + + if ($NormalizedGroupType -eq 'DynamicDistribution') { + # Compare Dynamic Distribution Group properties + $SetParams = @{ + Identity = $CheckExisting.Identity + } + + # Check recipient filter change + if ($CheckExisting.RecipientFilter -notmatch $groupobj.membershipRules) { + $SetParams.RecipientFilter = $groupobj.membershipRules + $ExoChangesNeeded.Add("RecipientFilter: '$($CheckExisting.RecipientFilter)' → '$($groupobj.membershipRules)'") + } + + # Only update if there are changes + if ($SetParams.Count -gt 1) { + $GraphRequest = New-ExoRequest -tenantid $tenant -cmdlet 'Set-DynamicDistributionGroup' -cmdParams $SetParams + } + + # Check external sender restrictions + if ($null -ne $groupobj.allowExternal) { + $currentAuthRequired = $CheckExisting.RequireSenderAuthenticationEnabled + $templateAuthRequired = [bool]!$groupobj.allowExternal + + if ($currentAuthRequired -ne $templateAuthRequired) { + $ExtParams = @{ + Identity = $CheckExisting.displayName + RequireSenderAuthenticationEnabled = $templateAuthRequired + } + $null = New-ExoRequest -tenantid $tenant -cmdlet 'Set-DynamicDistributionGroup' -cmdParams $ExtParams + $ExoChangesNeeded.Add("RequireSenderAuthenticationEnabled: '$currentAuthRequired' → '$templateAuthRequired'") + } } - $GraphRequest = New-ExoRequest -tenantid $tenant -cmdlet 'Set-DynamicDistributionGroup' -cmdParams $params + } else { - $Params = @{ - Identity = $groupobj.Displayname - Alias = $groupobj.username - Description = $groupobj.Description - PrimarySmtpAddress = $email - Type = $groupobj.groupType - RequireSenderAuthenticationEnabled = [bool]!$groupobj.AllowExternal + # Compare Regular Distribution Group properties + $SetParams = @{ + Identity = $CheckExisting.displayName + } + + # Check display name change + if ($CheckExisting.displayName -ne $groupobj.displayName) { + $SetParams.DisplayName = $groupobj.displayName + $ExoChangesNeeded.Add("DisplayName: '$($CheckExisting.displayName)' → '$($groupobj.displayName)'") + } + + # Check description change + if ($CheckExisting.description -ne $groupobj.description) { + $SetParams.Description = $groupobj.description + $ExoChangesNeeded.Add("Description: '$($CheckExisting.description)' → '$($groupobj.description)'") + } + + # Check external sender restrictions + if ($null -ne $groupobj.allowExternal) { + $currentAuthRequired = $CheckExisting.RequireSenderAuthenticationEnabled + $templateAuthRequired = [bool]!$groupobj.allowExternal + + if ($currentAuthRequired -ne $templateAuthRequired) { + $SetParams.RequireSenderAuthenticationEnabled = $templateAuthRequired + $ExoChangesNeeded.Add("RequireSenderAuthenticationEnabled: '$currentAuthRequired' → '$templateAuthRequired'") + } + } + + # Only update if there are changes + if ($SetParams.Count -gt 0) { + $GraphRequest = New-ExoRequest -tenantid $tenant -cmdlet 'Set-DistributionGroup' -cmdParams $SetParams } - $GraphRequest = New-ExoRequest -tenantid $tenant -cmdlet 'Set-DistributionGroup' -cmdParams $params + } + + # Log results + if ($ExoChangesNeeded.Count -gt 0) { + Write-LogMessage -API 'Standards' -tenant $tenant -message "Updated Exchange group '$($groupobj.displayName)' - Changes: $($ExoChangesNeeded -join ', ')" -Sev Info + } else { + Write-Information "Exchange group '$($groupobj.displayName)' already matches template - no update needed" } } - Write-LogMessage -API 'Standards' -tenant $tenant -message "Group exists $($groupobj.displayname). Updated to latest settings." -Sev 'Info' } } catch { @@ -121,12 +225,18 @@ function Invoke-CIPPStandardGroupTemplate { } } if ($Settings.report -eq $true) { - $Groups = $Settings.groupTemplate.JSON | ConvertFrom-Json -Depth 10 #check if all groups.displayName are in the existingGroups, if not $fieldvalue should contain all missing groups, else it should be true. - $MissingGroups = foreach ($Group in $Groups) { - $CheckExististing = $existingGroups | Where-Object -Property displayName -EQ $Group.displayname - if (!$CheckExististing) { - $Group.displayname + $MissingGroups = foreach ($Group in $GroupTemplates) { + if ($Group.groupType -eq 'dynamicDistribution') { + $CheckExisting = $DynamicDistros | Where-Object { $_.Name -eq $Group.displayName } + if (!$CheckExisting) { + $Group.displayName + } + } else { + $CheckExisting = $existingGroups | Where-Object { $_.displayName -eq $Group.displayName } + if (!$CheckExisting) { + $Group.displayName + } } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardLegacyEmailReportAddins.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardLegacyEmailReportAddins.ps1 new file mode 100644 index 000000000000..6b800db9a986 --- /dev/null +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardLegacyEmailReportAddins.ps1 @@ -0,0 +1,155 @@ +function Invoke-CIPPStandardLegacyEmailReportAddins { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) LegacyEmailReportAddins + .SYNOPSIS + (Label) Remove Legacy Email Report Add-ins + .DESCRIPTION + (Helptext) This standard removes the legacy "Report Message" and "Report Phishing" add-ins from Outlook. These have been superseded by newer reporting mechanisms. + (DocsDescription) This standard removes the legacy "Report Message" and "Report Phishing" add-ins from Outlook. These have been superseded by newer reporting mechanisms. + .NOTES + CAT + Exchange Standards + TAG + ADDEDCOMPONENT + IMPACT + Low Impact + ADDEDDATE + 2025-08-26 + POWERSHELLEQUIVALENT + Admin Center API + RECOMMENDEDBY + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + .LINK + https://docs.cipp.app/user-documentation/tenant/standards/list-standards + #> + + param($Tenant, $Settings) + + # Define the legacy add-ins to remove + $LegacyAddins = @( + @{ + AssetId = 'WA200002469' + ProductId = '3f32746a-0586-4c54-b8ce-d3b611c5b6c8' + Name = 'Report Phishing' + }, + @{ + AssetId = 'WA104381180' + ProductId = '6046742c-3aee-485e-a4ac-92ab7199db2e' + Name = 'Report Message' + } + ) + + try { + $CurrentApps = New-GraphGetRequest -scope 'https://admin.microsoft.com/.default' -TenantID $Tenant -Uri 'https://admin.microsoft.com/fd/addins/api/apps?workloads=AzureActiveDirectory,WXPO,MetaOS,Teams,SharePoint' + $InstalledApps = $CurrentApps.apps + } + catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the installed add-ins for $Tenant. Error: $ErrorMessage" -Sev Error + return + } + + # Check which legacy add-ins are currently installed + $AddinsToRemove = [System.Collections.Generic.List[PSCustomObject]]::new() + $InstalledLegacyAddins = [System.Collections.Generic.List[string]]::new() + + foreach ($LegacyAddin in $LegacyAddins) { + $InstalledAddin = $InstalledApps | Where-Object { $_.assetId -eq $LegacyAddin.AssetId -or $_.productId -eq $LegacyAddin.ProductId } + if ($InstalledAddin) { + $InstalledLegacyAddins.Add($LegacyAddin.Name) + $AddinsToRemove.Add([PSCustomObject]@{ + AppsourceAssetID = $LegacyAddin.AssetId + ProductID = $LegacyAddin.ProductId + Command = 'UNDEPLOY' + Workload = 'WXPO' + }) + } + } + + $StateIsCorrect = ($AddinsToRemove.Count -eq 0) + $RemediationPerformed = $false + + if ($Settings.remediate -eq $true) { + if ($StateIsCorrect -eq $true) { + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'Legacy Email Report Add-ins are already removed.' -Sev Info + } else { + foreach ($AddinToRemove in $AddinsToRemove) { + try { + $Body = @{ + Locale = 'en-US' + WorkloadManagementList = @($AddinToRemove) + } | ConvertTo-Json -Depth 10 -Compress + + $GraphRequest = @{ + tenantID = $Tenant + uri = 'https://admin.microsoft.com/fd/addins/api/apps' + scope = 'https://admin.microsoft.com/.default' + AsApp = $false + Type = 'POST' + ContentType = 'application/json; charset=utf-8' + Body = $Body + } + + $Response = New-GraphPostRequest @GraphRequest + $AddinName = ($LegacyAddins | Where-Object { $_.AssetId -eq $AddinToRemove.AppsourceAssetID }).Name + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Successfully initiated removal of $AddinName add-in" -Sev Info + $RemediationPerformed = $true + } catch { + $AddinName = ($LegacyAddins | Where-Object { $_.AssetId -eq $AddinToRemove.AppsourceAssetID }).Name + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Failed to remove $AddinName add-in" -Sev Error -LogData $_ + } + } + } + } + + # If we performed remediation and need to report/alert, get fresh state + if ($RemediationPerformed -and ($Settings.alert -eq $true -or $Settings.report -eq $true)) { + try { + $FreshApps = New-GraphGetRequest -scope 'https://admin.microsoft.com/.default' -TenantID $Tenant -Uri 'https://admin.microsoft.com/fd/addins/api/apps?workloads=AzureActiveDirectory,WXPO,MetaOS,Teams,SharePoint' + $FreshInstalledApps = $FreshApps.apps + + # Check fresh state + $FreshInstalledLegacyAddins = [System.Collections.Generic.List[string]]::new() + foreach ($LegacyAddin in $LegacyAddins) { + $InstalledAddin = $FreshInstalledApps | Where-Object { $_.assetId -eq $LegacyAddin.AssetId -or $_.productId -eq $LegacyAddin.ProductId } + if ($InstalledAddin) { + $FreshInstalledLegacyAddins.Add($LegacyAddin.Name) + } + } + + # Use fresh state for reporting/alerting + $StateIsCorrect = ($FreshInstalledLegacyAddins.Count -eq 0) + $InstalledLegacyAddins = $FreshInstalledLegacyAddins + } + catch { + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get fresh add-in state after remediation for $Tenant" -Sev Warning + } + } + + if ($Settings.alert -eq $true) { + if ($StateIsCorrect -eq $true) { + Write-LogMessage -API 'Standards' -tenant $tenant -message 'Legacy Email Report Add-ins are not installed.' -sev Info + } else { + $InstalledAddinsText = ($InstalledLegacyAddins -join ', ') + Write-StandardsAlert -message "Legacy Email Report Add-ins are still installed: $InstalledAddinsText" -tenant $tenant -standardName 'LegacyEmailReportAddins' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $tenant -message "Legacy Email Report Add-ins are still installed: $InstalledAddinsText" -sev Alert + } + } + + if ($Settings.report -eq $true) { + $ReportData = if ($StateIsCorrect) { + $true + } else { + @{ + InstalledLegacyAddins = $InstalledLegacyAddins + Status = 'Legacy add-ins still installed' + } + } + Set-CIPPStandardsCompareField -FieldName 'standards.LegacyEmailReportAddins' -FieldValue $ReportData -TenantFilter $Tenant + Add-CIPPBPAField -FieldName 'LegacyEmailReportAddins' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant + } +} diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1 index cf2459c7071c..95b345a2a534 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1 @@ -30,7 +30,7 @@ function Invoke-CIPPStandardMailboxRecipientLimits { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'MailboxRecipientLimits' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'MailboxRecipientLimits' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMalwareFilterPolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMalwareFilterPolicy.ps1 index dfc4135d4fa8..5825478c3f96 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMalwareFilterPolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMalwareFilterPolicy.ps1 @@ -40,7 +40,7 @@ function Invoke-CIPPStandardMalwareFilterPolicy { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'MalwareFilterPolicy' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'MalwareFilterPolicy' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMessageExpiration.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMessageExpiration.ps1 index af905f43d0a3..b3d2e22adb6d 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMessageExpiration.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMessageExpiration.ps1 @@ -28,7 +28,7 @@ function Invoke-CIPPStandardMessageExpiration { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'MessageExpiration' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'MessageExpiration' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOWAAttachmentRestrictions.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOWAAttachmentRestrictions.ps1 new file mode 100644 index 000000000000..1e2c2a925123 --- /dev/null +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOWAAttachmentRestrictions.ps1 @@ -0,0 +1,137 @@ +function Invoke-CIPPStandardOWAAttachmentRestrictions { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) OWAAttachmentRestrictions + .SYNOPSIS + (Label) Restrict Email Attachments on Unmanaged Devices + .DESCRIPTION + (Helptext) Restricts how users on unmanaged devices can interact with email attachments in Outlook on the web and new Outlook for Windows. Prevents downloading attachments or blocks viewing them entirely. + (DocsDescription) This standard configures the OWA mailbox policy to restrict access to email attachments on unmanaged devices. Users can be prevented from downloading attachments (but can view/edit via Office Online) or blocked from seeing attachments entirely. This helps prevent data exfiltration through email attachments on devices not managed by the organization. + .NOTES + CAT + Exchange Standards + TAG + "zero_trust" + "unmanaged_devices" + "attachment_restrictions" + "data_loss_prevention" + ADDEDCOMPONENT + {"type":"select","name":"standards.OWAAttachmentRestrictions.ConditionalAccessPolicy","label":"Attachment Restriction Policy","options":[{"label":"Read Only (View/Edit via Office Online, no download)","value":"ReadOnly"},{"label":"Read Only Plus Attachments Blocked (Cannot see attachments)","value":"ReadOnlyPlusAttachmentsBlocked"}],"defaultValue":"ReadOnlyPlusAttachmentsBlocked"} + IMPACT + Medium Impact + ADDEDDATE + 2025-08-22 + POWERSHELLEQUIVALENT + Set-OwaMailboxPolicy -Identity "OwaMailboxPolicy-Default" -ConditionalAccessPolicy ReadOnlyPlusAttachmentsBlocked + RECOMMENDEDBY + "Microsoft Zero Trust" + "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 + https://learn.microsoft.com/en-us/security/zero-trust/zero-trust-identity-device-access-policies-workloads#exchange-online-recommendations-for-zero-trust + #> + + param($Tenant, $Settings) + $TestResult = Test-CIPPStandardLicense -StandardName 'OWAAttachmentRestrictions' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + + if ($TestResult -eq $false) { + Write-Host "We're exiting as the correct license is not present for this standard." + return $true + } #we're done. + + # Input validation + $ValidPolicies = @('ReadOnly', 'ReadOnlyPlusAttachmentsBlocked') + if ($Settings.ConditionalAccessPolicy.value -notin $ValidPolicies) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "OWAAttachmentRestrictions: Invalid ConditionalAccessPolicy parameter set. Must be one of: $($ValidPolicies -join ', ')" -sev Error + return + } + + try { + # Get the default OWA mailbox policy + $CurrentPolicy = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-OwaMailboxPolicy' -cmdParams @{ Identity = 'OwaMailboxPolicy-Default' } + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the OWA Attachment Restrictions state for $Tenant. Error: $ErrorMessage" -Sev Error + return + } + + $StateIsCorrect = $CurrentPolicy.ConditionalAccessPolicy -eq $Settings.ConditionalAccessPolicy.value + + if ($Settings.remediate -eq $true) { + if ($StateIsCorrect) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "OWA attachment restrictions are already set to $($Settings.ConditionalAccessPolicy)" -sev Info + } else { + try { + $cmdParams = @{ + Identity = 'OwaMailboxPolicy-Default' + ConditionalAccessPolicy = $Settings.ConditionalAccessPolicy.value + } + + New-ExoRequest -tenantid $Tenant -cmdlet 'Set-OwaMailboxPolicy' -cmdParams $cmdParams + + $PolicyDescription = switch ($Settings.ConditionalAccessPolicy.value) { + 'ReadOnly' { 'Read Only (users can view/edit attachments via Office Online but cannot download)' } + 'ReadOnlyPlusAttachmentsBlocked' { 'Read Only Plus Attachments Blocked (users cannot see attachments at all)' } + } + + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully set OWA attachment restrictions to: $PolicyDescription" -sev Info + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Could not set OWA attachment restrictions. $($ErrorMessage.NormalizedError)" -sev Error + } + } + } + + if ($Settings.alert -eq $true) { + if ($StateIsCorrect) { + $PolicyDescription = switch ($Settings.ConditionalAccessPolicy.value) { + 'ReadOnly' { 'Read Only (view/edit via Office Online, no download)' } + 'ReadOnlyPlusAttachmentsBlocked' { 'Read Only Plus Attachments Blocked (cannot see attachments)' } + } + Write-LogMessage -API 'Standards' -tenant $Tenant -message "OWA attachment restrictions are correctly set to: $PolicyDescription" -sev Info + } else { + $CurrentDescription = switch ($CurrentPolicy.ConditionalAccessPolicy) { + 'ReadOnly' { 'Read Only (view/edit via Office Online, no download)' } + 'ReadOnlyPlusAttachmentsBlocked' { 'Read Only Plus Attachments Blocked (cannot see attachments)' } + $null { 'Not configured (full access to attachments)' } + default { $CurrentPolicy.ConditionalAccessPolicy } + } + + $RequiredDescription = switch ($Settings.ConditionalAccessPolicy.value) { + 'ReadOnly' { 'Read Only (view/edit via Office Online, no download)' } + 'ReadOnlyPlusAttachmentsBlocked' { 'Read Only Plus Attachments Blocked (cannot see attachments)' } + } + + $AlertMessage = "OWA attachment restrictions are set to '$CurrentDescription' but should be '$RequiredDescription'" + Write-StandardsAlert -message $AlertMessage -object @{ + CurrentPolicy = $CurrentPolicy.ConditionalAccessPolicy + RequiredPolicy = $Settings.ConditionalAccessPolicy + PolicyName = $CurrentPolicy.Name + CurrentDescription = $CurrentDescription + RequiredDescription = $RequiredDescription + } -tenant $Tenant -standardName 'OWAAttachmentRestrictions' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $Tenant -message $AlertMessage -sev Info + } + } + + if ($Settings.report -eq $true) { + if ($StateIsCorrect) { + Set-CIPPStandardsCompareField -FieldName 'standards.OWAAttachmentRestrictions' -FieldValue $true -TenantFilter $Tenant + Add-CIPPBPAField -FieldName 'OWAAttachmentRestrictions' -FieldValue $true -StoreAs bool -Tenant $Tenant + } else { + $ReportData = @{ + CurrentPolicy = $CurrentPolicy.ConditionalAccessPolicy + RequiredPolicy = $Settings.ConditionalAccessPolicy.value + PolicyName = $CurrentPolicy.Name + IsCompliant = $false + Description = 'OWA attachment restrictions not properly configured for unmanaged devices' + } + Set-CIPPStandardsCompareField -FieldName 'standards.OWAAttachmentRestrictions' -FieldValue $ReportData -TenantFilter $Tenant + Add-CIPPBPAField -FieldName 'OWAAttachmentRestrictions' -FieldValue $ReportData -StoreAs json -Tenant $Tenant + } + } +} diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOutBoundSpamAlert.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOutBoundSpamAlert.ps1 index 51049bddc07d..789c300c6233 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOutBoundSpamAlert.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOutBoundSpamAlert.ps1 @@ -31,7 +31,7 @@ function Invoke-CIPPStandardOutBoundSpamAlert { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'OutBoundSpamAlert' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'OutBoundSpamAlert' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishSimSpoofIntelligence.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishSimSpoofIntelligence.ps1 index 1cf8c915b826..23691f879c08 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishSimSpoofIntelligence.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishSimSpoofIntelligence.ps1 @@ -30,7 +30,7 @@ function Invoke-CIPPStandardPhishSimSpoofIntelligence { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'PhishSimSpoofIntelligence' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'PhishSimSpoofIntelligence' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishingSimulations.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishingSimulations.ps1 index 0fa3af87bf61..6cc3a314ea55 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishingSimulations.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishingSimulations.ps1 @@ -32,7 +32,7 @@ function Invoke-CIPPStandardPhishingSimulations { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'PhishingSimulations' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'PhishingSimulations' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardProfilePhotos.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardProfilePhotos.ps1 index 979f690a1222..c484f2006a17 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardProfilePhotos.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardProfilePhotos.ps1 @@ -29,7 +29,7 @@ function Invoke-CIPPStandardProfilePhotos { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'ProfilePhotos' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'ProfilePhotos' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineRequestAlert.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineRequestAlert.ps1 index 9a61e0478eb2..17778b5bf697 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineRequestAlert.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineRequestAlert.ps1 @@ -29,7 +29,7 @@ function Invoke-CIPPStandardQuarantineRequestAlert { #> param ($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'QuarantineRequestAlert' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'QuarantineRequestAlert' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineTemplate.ps1 index 02bf724072d4..1b821dbf7bd1 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineTemplate.ps1 @@ -40,7 +40,7 @@ function Invoke-CIPPStandardQuarantineTemplate { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'QuarantineTemplate' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'QuarantineTemplate' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRetentionPolicyTag.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRetentionPolicyTag.ps1 index a1135e0dc558..8cbdd779e730 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRetentionPolicyTag.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRetentionPolicyTag.ps1 @@ -29,7 +29,7 @@ function Invoke-CIPPStandardRetentionPolicyTag { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'RetentionPolicyTag' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'RetentionPolicyTag' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRotateDKIM.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRotateDKIM.ps1 index 4756d667c395..f3760a9bfdfa 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRotateDKIM.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRotateDKIM.ps1 @@ -31,7 +31,7 @@ function Invoke-CIPPStandardRotateDKIM { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'RotateDKIM' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'RotateDKIM' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeAttachmentPolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeAttachmentPolicy.ps1 index f3f0dc8c5ce5..459e13013550 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeAttachmentPolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeAttachmentPolicy.ps1 @@ -37,7 +37,7 @@ function Invoke-CIPPStandardSafeAttachmentPolicy { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'SafeAttachmentPolicy' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'SafeAttachmentPolicy' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksPolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksPolicy.ps1 index 6ae32ddd6a39..8abf1df83ace 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksPolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksPolicy.ps1 @@ -36,7 +36,7 @@ function Invoke-CIPPStandardSafeLinksPolicy { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'SafeLinksPolicy' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'SafeLinksPolicy' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksTemplatePolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksTemplatePolicy.ps1 index db323964664e..7de23f78f2a4 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksTemplatePolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksTemplatePolicy.ps1 @@ -29,7 +29,7 @@ function Invoke-CIPPStandardSafeLinksTemplatePolicy { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'SafeLinksTemplatePolicy' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'SafeLinksTemplatePolicy' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeSendersDisable.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeSendersDisable.ps1 index d246ed95746d..182449f8ab18 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeSendersDisable.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeSendersDisable.ps1 @@ -31,7 +31,7 @@ function Invoke-CIPPStandardSafeSendersDisable { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'SafeSendersDisable' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'SafeSendersDisable' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendFromAlias.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendFromAlias.ps1 index 28692ae363c5..536f0ce8afd6 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendFromAlias.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendFromAlias.ps1 @@ -29,7 +29,7 @@ function Invoke-CIPPStandardSendFromAlias { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'SendFromAlias' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'SendFromAlias' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendReceiveLimitTenant.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendReceiveLimitTenant.ps1 index 958e822a9c9d..82d75d8bafff 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendReceiveLimitTenant.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendReceiveLimitTenant.ps1 @@ -30,7 +30,7 @@ function Invoke-CIPPStandardSendReceiveLimitTenant { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'SendReceiveLimitTenant' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'SendReceiveLimitTenant' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardShortenMeetings.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardShortenMeetings.ps1 index 2d567060ab95..2b74ec1f1ad5 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardShortenMeetings.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardShortenMeetings.ps1 @@ -31,7 +31,7 @@ function Invoke-CIPPStandardShortenMeetings { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'ShortenMeetings' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'ShortenMeetings' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 index 3b811a9a712e..3604327ff5bc 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 @@ -51,7 +51,7 @@ function Invoke-CIPPStandardSpamFilterPolicy { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'SpamFilterPolicy' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'SpamFilterPolicy' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpoofWarn.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpoofWarn.ps1 index 15a49616c884..763bd36054d3 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpoofWarn.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpoofWarn.ps1 @@ -33,7 +33,7 @@ function Invoke-CIPPStandardSpoofWarn { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'SpoofWarn' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'SpoofWarn' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMeetingsByDefault.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMeetingsByDefault.ps1 index bcb2881ab356..569d97f465cf 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMeetingsByDefault.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMeetingsByDefault.ps1 @@ -29,7 +29,7 @@ function Invoke-CIPPStandardTeamsMeetingsByDefault { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsMeetingsByDefault' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsMeetingsByDefault' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTransportRuleTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTransportRuleTemplate.ps1 index 3f85ccbc01d5..ed82eeba51f9 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTransportRuleTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTransportRuleTemplate.ps1 @@ -26,7 +26,7 @@ function Invoke-CIPPStandardTransportRuleTemplate { https://docs.cipp.app/user-documentation/tenant/standards/list-standards #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'TransportRuleTemplate' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'TransportRuleTemplate' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTwoClickEmailProtection.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTwoClickEmailProtection.ps1 index b43fa7fdb673..b1bb5ff5c84c 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTwoClickEmailProtection.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTwoClickEmailProtection.ps1 @@ -29,7 +29,7 @@ function Invoke-CIPPStandardTwoClickEmailProtection { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'TwoClickEmailProtection' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'TwoClickEmailProtection' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserSubmissions.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserSubmissions.ps1 index 66833d43dd9c..b5f9dc674954 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserSubmissions.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserSubmissions.ps1 @@ -30,7 +30,7 @@ function Invoke-CIPPStandardUserSubmissions { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'UserSubmissions' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'UserSubmissions' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardcalDefault.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardcalDefault.ps1 index 2c490636c5d6..88ee44f103d7 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardcalDefault.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardcalDefault.ps1 @@ -32,7 +32,7 @@ function Invoke-CIPPStandardcalDefault { param($Tenant, $Settings, $QueueItem) ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'calDefault' - $TestResult = Test-CIPPStandardLicense -StandardName 'calDefault' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'calDefault' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1 b/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1 index 1e13a1d5f25c..04fcdfb76c9b 100644 --- a/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1 +++ b/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1 @@ -372,22 +372,26 @@ function Test-CIPPAuditLogRules { $MatchedRules = [System.Collections.Generic.List[string]]::new() $DataToProcess = foreach ($clause in $Where) { - $ClauseStartTime = Get-Date - Write-Warning "Webhook: Processing clause: $($clause.clause)" - $ReturnedData = $ProcessedData | Where-Object { Invoke-Expression $clause.clause } - if ($ReturnedData) { - Write-Warning "Webhook: There is matching data: $(($ReturnedData.operation | Select-Object -Unique) -join ', ')" - $ReturnedData = foreach ($item in $ReturnedData) { - $item.CIPPAction = $clause.expectedAction - $item.CIPPClause = $clause.CIPPClause -join ' and ' - $MatchedRules.Add($clause.CIPPClause -join ' and ') - $item + try { + $ClauseStartTime = Get-Date + Write-Warning "Webhook: Processing clause: $($clause.clause)" + $ReturnedData = $ProcessedData | Where-Object { Invoke-Expression $clause.clause } + if ($ReturnedData) { + Write-Warning "Webhook: There is matching data: $(($ReturnedData.operation | Select-Object -Unique) -join ', ')" + $ReturnedData = foreach ($item in $ReturnedData) { + $item.CIPPAction = $clause.expectedAction + $item.CIPPClause = $clause.CIPPClause -join ' and ' + $MatchedRules.Add($clause.CIPPClause -join ' and ') + $item + } } + $ClauseEndTime = Get-Date + $ClauseSeconds = ($ClauseEndTime - $ClauseStartTime).TotalSeconds + Write-Warning "Task took $ClauseSeconds seconds for clause: $($clause.clause)" + $ReturnedData + } catch { + Write-Warning "Error processing clause: $($clause.clause): $($_.Exception.Message)" } - $ClauseEndTime = Get-Date - $ClauseSeconds = ($ClauseEndTime - $ClauseStartTime).TotalSeconds - Write-Warning "Task took $ClauseSeconds seconds for clause: $($clause.clause)" - $ReturnedData } $Results.MatchedRules = @($MatchedRules | Select-Object -Unique) $Results.MatchedLogs = ($DataToProcess | Measure-Object).Count diff --git a/Modules/CippEntrypoints/CippEntrypoints.psm1 b/Modules/CippEntrypoints/CippEntrypoints.psm1 index 524a3f6fd0ad..2166fd1d1534 100644 --- a/Modules/CippEntrypoints/CippEntrypoints.psm1 +++ b/Modules/CippEntrypoints/CippEntrypoints.psm1 @@ -40,7 +40,7 @@ function Receive-CippHttpTrigger { $Request = $Request | ConvertTo-Json -Depth 100 | ConvertFrom-Json Set-Location (Get-Item $PSScriptRoot).Parent.Parent.FullName $FunctionName = 'Invoke-{0}' -f $Request.Params.CIPPEndpoint - Write-Information "Function: $($Request.Params.CIPPEndpoint)" + Write-Information "API: $($Request.Params.CIPPEndpoint)" $HttpTrigger = @{ Request = [pscustomobject]($Request) @@ -61,7 +61,7 @@ function Receive-CippHttpTrigger { }) return } - + try { Write-Information "Access: $Access" if ($Access) { @@ -179,6 +179,7 @@ function Receive-CippActivityTrigger { Write-Warning "Hey Boo, the activity function is running. Here's some info: $($Item | ConvertTo-Json -Depth 10 -Compress)" try { $Start = Get-Date + $Output = $null Set-Location (Get-Item $PSScriptRoot).Parent.Parent.FullName if ($Item.QueueId) { @@ -202,7 +203,7 @@ function Receive-CippActivityTrigger { $FunctionName = 'Push-{0}' -f $Item.FunctionName try { Write-Warning "Activity starting Function: $FunctionName." - Invoke-Command -ScriptBlock { & $FunctionName -Item $Item } + $Output = Invoke-Command -ScriptBlock { & $FunctionName -Item $Item } Write-Warning "Activity completed Function: $FunctionName." if ($TaskStatus) { $QueueTask.Status = 'Completed' @@ -244,7 +245,13 @@ function Receive-CippActivityTrigger { $null = Set-CippQueueTask @QueueTask } } - return $true + + # Return the captured output if it exists and is not null, otherwise return $true + if ($null -ne $Output -and $Output -ne '') { + return $Output + } else { + return $true + } } function Receive-CIPPTimerTrigger { @@ -291,7 +298,7 @@ function Receive-CIPPTimerTrigger { $Results = Invoke-Command -ScriptBlock { & $Function.Command @Parameters } if ($Results -match '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$') { - $FunctionStatus.OrchestratorId = $Results + $FunctionStatus.OrchestratorId = $Results -join ',' $Status = 'Started' } else { $Status = 'Completed' diff --git a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 index 155a400aea6a..493809046947 100644 --- a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 +++ b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 @@ -1565,6 +1565,16 @@ function Invoke-NinjaOneTenantSync { Name = 'Azure Portal' Link = "https://portal.azure.com/$($customer.defaultDomainName)" Icon = 'fas fa-server' + }, + @{ + Name = 'Power Platform Portal' + Link = "https://admin.powerplatform.microsoft.com/account/login/$($Customer.customerId)" + Icon = 'fa-solid fa-robot' + }, + @{ + Name = 'Power BI Portal' + Link = "https://app.powerbi.com/admin-portal?ctid=$($Customer.customerId)" + Icon = 'fas fa-bar-chart' } ) diff --git a/cspell.json b/cspell.json index 56a59eca16f4..5e6ccfb08f36 100644 --- a/cspell.json +++ b/cspell.json @@ -20,6 +20,7 @@ "endswith", "entra", "Entra", + "eula", "exploitability", "gdap", "GDAP", diff --git a/openapi.json b/openapi.json index c94c00948d65..013d331ba238 100644 --- a/openapi.json +++ b/openapi.json @@ -3640,6 +3640,259 @@ } } }, + "/ListContactPermissions": { + "get": { + "description": "ListContactPermissions - Retrieves contact folder permissions for a specified user", + "summary": "ListContactPermissions", + "tags": [ + "GET" + ], + "parameters": [ + { + "required": true, + "schema": { + "type": "string" + }, + "name": "UserID", + "in": "query", + "description": "The user ID to retrieve contact permissions for" + }, + { + "required": true, + "schema": { + "type": "string" + }, + "name": "tenantFilter", + "in": "query", + "description": "The tenant filter to specify which tenant" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "Identity": { + "type": "string", + "description": "The identity of the contact folder" + }, + "User": { + "type": "string", + "description": "The user who has permissions" + }, + "AccessRights": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The access rights granted to the user" + }, + "FolderName": { + "type": "string", + "description": "The name of the contact folder" + }, + "MailboxInfo": { + "type": "object", + "description": "Information about the mailbox" + } + } + } + } + } + }, + "description": "Successfully retrieved contact permissions" + }, + "403": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + }, + "description": "Forbidden - insufficient permissions" + } + } + } + }, + "/ExecModifyContactPerms": { + "post": { + "description": "ExecModifyContactPerms - Modifies contact folder permissions for a specified user", + "summary": "ExecModifyContactPerms", + "tags": [ + "POST" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "userID", + "tenantFilter", + "permissions" + ], + "properties": { + "userID": { + "type": "string", + "description": "The user ID whose contact permissions will be modified" + }, + "tenantFilter": { + "type": "string", + "description": "The tenant filter to specify which tenant" + }, + "permissions": { + "type": "array", + "description": "Array of permission objects to apply", + "items": { + "type": "object", + "properties": { + "PermissionLevel": { + "type": "object", + "properties": { + "value": { + "type": "string", + "description": "Permission level (e.g., Owner, PublishingEditor, Editor, PublishingAuthor, Author, NonEditingAuthor, Reviewer, Contributor, AvailabilityOnly, LimitedDetails)" + } + } + }, + "Modification": { + "type": "string", + "description": "Type of modification (Add, Remove)", + "enum": ["Add", "Remove"] + }, + "UserID": { + "type": "array", + "description": "Array of target users to grant/remove permissions", + "items": { + "type": "object", + "properties": { + "value": { + "type": "string", + "description": "User ID or email address" + } + } + } + }, + "CanViewPrivateItems": { + "type": "boolean", + "description": "Whether the user can view private items", + "default": false + }, + "FolderName": { + "type": "string", + "description": "Name of the contact folder", + "default": "Contact" + }, + "SendNotificationToUser": { + "type": "boolean", + "description": "Whether to send notification to the user", + "default": false + } + }, + "required": [ + "PermissionLevel", + "Modification", + "UserID" + ] + } + } + } + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Results": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Array of result messages for each permission operation" + } + } + } + } + }, + "description": "Successfully processed contact permission modifications" + }, + "400": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Results": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Error messages" + } + } + } + } + }, + "description": "Bad Request - Missing required parameters" + }, + "404": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Results": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Error messages" + } + } + } + } + }, + "description": "Not Found - User ID not found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Results": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Error messages" + } + } + } + } + }, + "description": "Internal Server Error - Operation failed" + } + } + } + }, "/ListMailboxRules": { "get": { "description": "ListMailboxRules", diff --git a/profile.ps1 b/profile.ps1 index 4c02022def7a..fbbcb0ed9df0 100644 --- a/profile.ps1 +++ b/profile.ps1 @@ -37,17 +37,19 @@ try { Set-Location -Path $PSScriptRoot $CurrentVersion = (Get-Content .\version_latest.txt).trim() $Table = Get-CippTable -tablename 'Version' -Write-Information "Function: $($env:WEBSITE_SITE_NAME) Version: $CurrentVersion" +Write-Information "Function App: $($env:WEBSITE_SITE_NAME) Version: $CurrentVersion" $LastStartup = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq 'Version' and RowKey eq '$($env:WEBSITE_SITE_NAME)'" if (!$LastStartup -or $CurrentVersion -ne $LastStartup.Version) { Write-Information "Version has changed from $($LastStartup.Version ?? 'None') to $CurrentVersion" if ($LastStartup) { $LastStartup.Version = $CurrentVersion + $LastStartup | Add-Member -MemberType NoteProperty -Name 'PSVersion' -Value $PSVersionTable.PSVersion.ToString() } else { $LastStartup = [PSCustomObject]@{ PartitionKey = 'Version' RowKey = $env:WEBSITE_SITE_NAME Version = $CurrentVersion + PSVersion = $PSVersionTable.PSVersion.ToString() } } Update-AzDataTableEntity @Table -Entity $LastStartup -Force -ErrorAction SilentlyContinue diff --git a/version_latest.txt b/version_latest.txt index 9c78b761ea12..a2f28f43be33 100644 --- a/version_latest.txt +++ b/version_latest.txt @@ -1 +1 @@ -8.3.2 +8.4.0