From 7b26b118dc51b959f171d16bad737f09ab2dc8d7 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Thu, 30 Apr 2026 20:01:06 +0200 Subject: [PATCH 01/36] fix(compare): handle non-catalog null results --- Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 | 2 +- .../Endpoint/MEM/Invoke-ExecCompareIntunePolicy.ps1 | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 b/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 index d4a19075ac9e..fc9902d80823 100644 --- a/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 +++ b/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 @@ -12,7 +12,7 @@ function Compare-CIPPIntuneObject { [Parameter(Mandatory = $false)] [string[]]$CompareType = @() ) - if ($CompareType -ne 'Catalog') { + if ($CompareType -notcontains 'Catalog') { $defaultExcludeProperties = @( 'id', 'createdDateTime', diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ExecCompareIntunePolicy.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ExecCompareIntunePolicy.ps1 index 09158ce20a60..26f924e6e195 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ExecCompareIntunePolicy.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ExecCompareIntunePolicy.ps1 @@ -157,7 +157,8 @@ function Invoke-ExecCompareIntunePolicy { } # Run the comparison - $ComparisonResults = @(Compare-CIPPIntuneObject @CompareParams) + $CompareResult = Compare-CIPPIntuneObject @CompareParams + $ComparisonResults = if ($null -eq $CompareResult) { @() } else { @($CompareResult) } $ResultBody = @{ Results = $ComparisonResults From ece775bd1b3b4aaf518a009b7409555c111faa61 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Fri, 1 May 2026 00:52:52 +0200 Subject: [PATCH 02/36] feat(intune): extend ListIntunePolicy for admin templates --- .../Endpoint/MEM/Invoke-ListIntunePolicy.ps1 | 71 ++++++++++++++++++- 1 file changed, 68 insertions(+), 3 deletions(-) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntunePolicy.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntunePolicy.ps1 index fd199a71aa7a..c01f718c2a5f 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntunePolicy.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntunePolicy.ps1 @@ -12,12 +12,14 @@ function Invoke-ListIntunePolicy { $TenantFilter = $Request.Query.TenantFilter $id = $Request.Query.ID $URLName = $Request.Query.URLName + $DefinitionIds = $Request.Query.DefinitionIds $UseReportDB = $Request.Query.UseReportDB $IncludeSettingDefinitions = [System.Convert]::ToBoolean($Request.Query.IncludeSettingDefinitions ?? 'false') + $IsGroupPolicyDefinitionLookup = ($URLName -ieq 'GroupPolicyDefinitions') -and -not [string]::IsNullOrWhiteSpace($DefinitionIds) try { # Return cached report data when AllTenants is requested or UseReportDB is set - if ($TenantFilter -eq 'AllTenants' -or $UseReportDB -eq 'true') { + if (-not $IsGroupPolicyDefinitionLookup -and ($TenantFilter -eq 'AllTenants' -or $UseReportDB -eq 'true')) { try { $GraphRequest = Get-CIPPIntunePolicyReport -TenantFilter $TenantFilter -ErrorAction Stop $StatusCode = [HttpStatusCode]::OK @@ -31,8 +33,30 @@ function Invoke-ListIntunePolicy { }) } - if ($ID) { - if ($URLName -ieq 'ConfigurationPolicies' -or $URLName -ieq 'configurationPolicies') { + if ($IsGroupPolicyDefinitionLookup) { + $DefinitionIdList = @($DefinitionIds -split ',' | ForEach-Object { $_.Trim() } | Where-Object { Test-IsGuid -String $_ } | Select-Object -Unique) + + if ($DefinitionIdList.Count -eq 0) { + $GraphRequest = @() + } else { + $DefinitionRequests = [System.Collections.Generic.List[object]]::new() + $DefinitionIndex = 0 + + foreach ($DefinitionId in $DefinitionIdList) { + $RequestId = "definition$DefinitionIndex" + $DefinitionRequests.Add([PSCustomObject]@{ + id = $RequestId + method = 'GET' + url = "/deviceManagement/groupPolicyDefinitions('$DefinitionId')?`$expand=presentations" + }) + $DefinitionIndex++ + } + + $DefinitionResults = New-GraphBulkRequest -Requests @($DefinitionRequests) -tenantid $TenantFilter + $GraphRequest = $DefinitionResults | Where-Object { $_.status -eq 200 -and $_.body.id } | ForEach-Object { $_.body } + } + } elseif ($ID) { + if ($URLName -ieq 'ConfigurationPolicies') { $GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/configurationPolicies('$ID')?`$expand=settings" -tenantid $TenantFilter if ($IncludeSettingDefinitions -and $GraphRequest.settings) { @@ -65,6 +89,47 @@ function Invoke-ListIntunePolicy { } } } + } elseif ($URLName -ieq 'GroupPolicyConfigurations') { + $GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/groupPolicyConfigurations('$ID')" -tenantid $TenantFilter + + if ($IncludeSettingDefinitions) { + $DefinitionValuesResponse = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/groupPolicyConfigurations('$ID')/definitionValues?`$expand=definition" -tenantid $TenantFilter + $DefinitionValues = @($DefinitionValuesResponse.value ?? $DefinitionValuesResponse) + $GraphRequest | Add-Member -NotePropertyName definitionValues -NotePropertyValue $DefinitionValues -Force + + if ($DefinitionValues.Count -gt 0) { + $PresentationRequests = [System.Collections.Generic.List[object]]::new() + $DefinitionValueLookup = @{} + $DefinitionValueIdMap = @{} + $DefinitionValueIndex = 0 + + foreach ($DefinitionValue in $DefinitionValues) { + if ($DefinitionValue.id) { + $RequestId = "definitionValue$DefinitionValueIndex" + $DefinitionValueIdMap[$RequestId] = $DefinitionValue.id + $DefinitionValueLookup[$DefinitionValue.id] = $DefinitionValue + $PresentationRequests.Add([PSCustomObject]@{ + id = $RequestId + method = 'GET' + url = "/deviceManagement/groupPolicyConfigurations('$ID')/definitionValues('$($DefinitionValue.id)')/presentationValues?`$expand=presentation" + }) + $DefinitionValueIndex++ + } + } + + if ($PresentationRequests.Count -gt 0) { + $PresentationResults = New-GraphBulkRequest -Requests @($PresentationRequests) -tenantid $TenantFilter + foreach ($PresentationResult in $PresentationResults) { + $DefinitionValueId = $DefinitionValueIdMap[$PresentationResult.id] + $DefinitionValue = $DefinitionValueLookup[$DefinitionValueId] + if ($DefinitionValue) { + $PresentationValues = @($PresentationResult.body.value ?? $PresentationResult.body) + $DefinitionValue | Add-Member -NotePropertyName presentationValues -NotePropertyValue $PresentationValues -Force + } + } + } + } + } } else { $GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$($URLName)('$ID')" -tenantid $TenantFilter } From 45c8f59872453db2fa35ade73ca4df788c64f947 Mon Sep 17 00:00:00 2001 From: Brian Simpson <50429915+bmsimp@users.noreply.github.com> Date: Sun, 3 May 2026 12:24:55 -0500 Subject: [PATCH 03/36] Fix TeamsMeetingRecordingExpiration drift report showing Current=true The report block overwrote the actual current expiration day count with the boolean literal $true whenever StateIsCorrect was true, then wrote that boolean into the compare field as the "Current" value. This caused the drift page to render Expected=120 / Current= true and flag a deviation even when the policy was correctly set -- while the remediate block (which used the unmangled value) logged "already set to 120 days". Removing the conditional reassignment makes the report always emit the real numeric value, so when state matches expected the drift page goes green instead of showing a phantom deviation. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Invoke-CIPPStandardTeamsMeetingRecordingExpiration.ps1 | 1 - 1 file changed, 1 deletion(-) diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsMeetingRecordingExpiration.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsMeetingRecordingExpiration.ps1 index e84885986aee..e093179116f2 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsMeetingRecordingExpiration.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsMeetingRecordingExpiration.ps1 @@ -91,7 +91,6 @@ function Invoke-CIPPStandardTeamsMeetingRecordingExpiration { if ($Settings.report -eq $true) { Add-CIPPBPAField -FieldName 'TeamsMeetingRecordingExpiration' -FieldValue $CurrentExpirationDays -StoreAs string -Tenant $Tenant - $CurrentExpirationDays = if ($StateIsCorrect) { $true } else { $CurrentExpirationDays } $CurrentValue = @{ MeetingRecordingExpirationDays = $CurrentExpirationDays } From 512d87c2aa15d4d9ae25225def7e6e08dd714f27 Mon Sep 17 00:00:00 2001 From: Brian Simpson <50429915+bmsimp@users.noreply.github.com> Date: Sun, 3 May 2026 12:30:09 -0500 Subject: [PATCH 04/36] Fix SPFileRequests drift report showing pre-remediation values The standard fetches $CurrentState from SPO once at the top, then the remediate block calls Set-CIPPSPOTenant to mutate the live state but never refreshes $CurrentState. When both remediate and report run in the same pass, the report block builds CurrentValue from the stale pre-remediation $CurrentState and writes it into the drift compare field -- producing Expected=true / Current=false on the manage drift page even though the live SPO state is now correct (and the logbook says "Successfully set File Requests..."). After a successful Set-CIPPSPOTenant, mirror the just-applied properties into $CurrentState so the report block writes the post-remediation values. The catch path is untouched, so failed remediation correctly leaves the original state in the report. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Standards/Invoke-CIPPStandardSPFileRequests.ps1 | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPFileRequests.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPFileRequests.ps1 index 099d196ac80e..5c3169485453 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPFileRequests.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPFileRequests.ps1 @@ -97,6 +97,15 @@ function Invoke-CIPPStandardSPFileRequests { $CurrentState | Set-CIPPSPOTenant -Properties $Properties + # Reflect the just-applied state in-memory so the report block does not write + # the pre-remediation values into the drift compare field. + $CurrentState.CoreRequestFilesLinkEnabled = $WantedState + $CurrentState.OneDriveRequestFilesLinkEnabled = $WantedState + if ($null -ne $ExpirationDays -and $WantedState -eq $true) { + $CurrentState.CoreRequestFilesLinkExpirationInDays = $ExpirationDays + $CurrentState.OneDriveRequestFilesLinkExpirationInDays = $ExpirationDays + } + $ExpirationMessage = if ($null -ne $ExpirationDays -and $WantedState -eq $true) { " with $ExpirationDays day expiration" } else { '' } Write-LogMessage -API 'Standards' -tenant $tenant -message "Successfully set File Requests to $HumanReadableState$ExpirationMessage" -sev Info } catch { From 2be775b64dcd88fa112a236d9b8b61a4ea3d2e94 Mon Sep 17 00:00:00 2001 From: Brian Simpson <50429915+bmsimp@users.noreply.github.com> Date: Sun, 3 May 2026 12:59:59 -0500 Subject: [PATCH 05/36] Fix DisableSelfServiceLicenses autoclaim reading wrong property The autoclaim GET endpoint at admin.microsoft.com returns the policy state under tenantPolicyValue, not policyValue: { "policyId": "Autoclaim", "tenantPolicyValue": "Disabled", "tenantId": "..." } CIPP was reading $AutoClaimPolicy.policyValue (which does not exist on the response object) so it always evaluated to $null, causing: - the initial Compare to find a diff vs the Expected 'Disabled', - remediation to fire every run, - the post-remediation re-fetch to also read null, - the drift compare-field to be written with Value=null, - the manage drift page to show a phantom deviation despite the log saying "Changed Self Service status for product 'Trial Autoclaim - autoclaim' from '' to 'Disabled'". The POST body still uses {"policyValue": "Disabled"} -- Microsoft's read/write schemas are asymmetric on this endpoint, so only the read paths needed the property name corrected. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 index f1bcecf87877..43c37afa8fa4 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 @@ -78,7 +78,7 @@ function Invoke-CIPPStandardDisableSelfServiceLicenses { $CurrentValues.Add([PSCustomObject]@{ productName = 'Trial Autoclaim' productId = 'autoclaim' - policyValue = if ($null -eq $AutoClaimPolicy.policyValue) { 'Disabled' } else { $AutoClaimPolicy.policyValue } + policyValue = $AutoClaimPolicy.tenantPolicyValue ?? 'Disabled' }) } catch { Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to retrieve trial autoclaim policy: $($_.Exception.Message)" -sev Error @@ -178,7 +178,7 @@ function Invoke-CIPPStandardDisableSelfServiceLicenses { $CurrentValues.Add([PSCustomObject]@{ productName = 'Trial Autoclaim' productId = 'autoclaim' - policyValue = if ($null -eq $AutoClaimPolicy.policyValue) { 'Disabled' } else { $AutoClaimPolicy.policyValue } + policyValue = $AutoClaimPolicy.tenantPolicyValue ?? 'Disabled' }) } catch { Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to retrieve trial autoclaim policy after remediation: $($_.Exception.Message)" -sev Error From 7aeeffcb48b080685c180e8431e3128d3433a082 Mon Sep 17 00:00:00 2001 From: Brian Simpson <50429915+bmsimp@users.noreply.github.com> Date: Sun, 3 May 2026 13:04:58 -0500 Subject: [PATCH 06/36] Fix TeamsFederationConfiguration drift report ordering mismatch The state check uses Compare-Object (order-insensitive) so $StateIsCorrect correctly reported "Federation Configuration already set." But the report block wrote the raw arrays to the compare field, where the current side was alphabetically sorted upstream and the expected side was in user-input order from $Settings.DomainList. The drift page diffs the JSON arrays literally, so identical sets in different orders rendered as a phantom deviation. Sort the parsed expected arrays (AllowedDomainsAsAList for AllowSpecificExternal, BlockedDomains for BlockSpecificExternal) at the source so they match the already-sorted current values. Set-CsTenantFederationConfiguration is set-semantic on the Microsoft side, so order does not affect remediation behavior. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Invoke-CIPPStandardTeamsFederationConfiguration.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsFederationConfiguration.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsFederationConfiguration.ps1 index ab245b7fd939..425aa552a2b6 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsFederationConfiguration.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsFederationConfiguration.ps1 @@ -75,7 +75,7 @@ function Invoke-CIPPStandardTeamsFederationConfiguration { $AllowedDomains = $null $BlockedDomains = @() if ($null -ne $Settings.DomainList) { - $AllowedDomainsAsAList = @($Settings.DomainList).Split(',').Trim() + $AllowedDomainsAsAList = @($Settings.DomainList).Split(',').Trim() | Sort-Object } else { $AllowedDomainsAsAList = @() } @@ -85,7 +85,7 @@ function Invoke-CIPPStandardTeamsFederationConfiguration { $AllowedDomains = $AllowAllKnownDomains $AllowedDomainsAsAList = @() if ($null -ne $Settings.DomainList) { - $BlockedDomains = @($Settings.DomainList).Split(',').Trim() + $BlockedDomains = @($Settings.DomainList).Split(',').Trim() | Sort-Object } else { $BlockedDomains = @() } From 27c08abc9a2d81b0ae133f8f66ba2602c10353d4 Mon Sep 17 00:00:00 2001 From: Brian Simpson <50429915+bmsimp@users.noreply.github.com> Date: Sun, 3 May 2026 13:09:55 -0500 Subject: [PATCH 07/36] Fix SafeLinksPolicy and MalwareFilterPolicy drift report ordering Same shape as the TeamsFederationConfiguration fix: the state check uses Compare-Object (order-insensitive) so $StateIsCorrect correctly reports "already correctly configured", but the report block writes the raw arrays to the compare field where Current and Expected come from different sources and may be ordered differently. The drift page diffs the JSON arrays literally, producing phantom deviations even though the policies are correctly applied. SafeLinksPolicy.DoNotRewriteUrls: Current was $CurrentState.DoNotRewriteUrls (raw API), Expected was $Settings.DoNotRewriteUrls.value (raw user input). Sort both in the report block so they match regardless of input or API order. MalwareFilterPolicy.FileTypes: Current was $CurrentState.FileTypes (raw API), Expected was $ExpectedFileTypes ($DefaultFileTypes + user-supplied OptionalFileTypes split). Sort both in the report block. Set-SafeLinksPolicy and Set-MalwareFilterPolicy treat these lists as sets on the Microsoft side, so order does not affect remediation behavior -- only the drift display. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Standards/Invoke-CIPPStandardMalwareFilterPolicy.ps1 | 4 ++-- .../Public/Standards/Invoke-CIPPStandardSafeLinksPolicy.ps1 | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardMalwareFilterPolicy.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardMalwareFilterPolicy.ps1 index 978103c47d88..661ee92e0760 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardMalwareFilterPolicy.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardMalwareFilterPolicy.ps1 @@ -220,7 +220,7 @@ function Invoke-CIPPStandardMalwareFilterPolicy { Name = $CurrentState.Name EnableFileFilter = $CurrentState.EnableFileFilter FileTypeAction = $CurrentState.FileTypeAction - FileTypes = $CurrentState.FileTypes + FileTypes = @($CurrentState.FileTypes | Sort-Object) ZapEnabled = $CurrentState.ZapEnabled QuarantineTag = $CurrentState.QuarantineTag EnableInternalSenderAdminNotifications = $CurrentState.EnableInternalSenderAdminNotifications @@ -238,7 +238,7 @@ function Invoke-CIPPStandardMalwareFilterPolicy { Name = $PolicyName EnableFileFilter = $true FileTypeAction = $FileTypeAction - FileTypes = $ExpectedFileTypes + FileTypes = @($ExpectedFileTypes | Sort-Object) ZapEnabled = $true QuarantineTag = $Settings.QuarantineTag EnableInternalSenderAdminNotifications = $Settings.EnableInternalSenderAdminNotifications diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSafeLinksPolicy.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSafeLinksPolicy.ps1 index 04527ce524d6..132389c0f15f 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSafeLinksPolicy.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSafeLinksPolicy.ps1 @@ -231,7 +231,7 @@ function Invoke-CIPPStandardSafeLinksPolicy { DeliverMessageAfterScan = $CurrentState.DeliverMessageAfterScan DisableUrlRewrite = $CurrentState.DisableUrlRewrite EnableOrganizationBranding = $CurrentState.EnableOrganizationBranding - DoNotRewriteUrls = $CurrentState.DoNotRewriteUrls + DoNotRewriteUrls = @($CurrentState.DoNotRewriteUrls | Sort-Object) } $ExpectedValue = @{ Name = $PolicyName @@ -245,7 +245,7 @@ function Invoke-CIPPStandardSafeLinksPolicy { DeliverMessageAfterScan = $true DisableUrlRewrite = $Settings.DisableUrlRewrite EnableOrganizationBranding = $Settings.EnableOrganizationBranding - DoNotRewriteUrls = $Settings.DoNotRewriteUrls.value ?? @() + DoNotRewriteUrls = @(($Settings.DoNotRewriteUrls.value ?? @()) | Sort-Object) } Set-CIPPStandardsCompareField -FieldName 'standards.SafeLinksPolicy' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } From 3beb6226f86e5b05661160a8fcace5d7931e161c Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 7 May 2026 19:10:37 +0800 Subject: [PATCH 08/36] Better queue tracking --- .../Graph Requests/Push-ListGraphRequestQueue.ps1 | 9 +++++++++ Modules/CIPPCore/Public/CippQueue/New-CippQueueEntry.ps1 | 1 + .../Orchestrator Functions/Start-CIPPOrchestrator.ps1 | 8 +++----- .../Public/GraphRequests/Get-GraphRequestList.ps1 | 5 +++-- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Graph Requests/Push-ListGraphRequestQueue.ps1 b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Graph Requests/Push-ListGraphRequestQueue.ps1 index 9c838711e42c..fb6c7fe15bcc 100644 --- a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Graph Requests/Push-ListGraphRequestQueue.ps1 +++ b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Graph Requests/Push-ListGraphRequestQueue.ps1 @@ -63,6 +63,15 @@ function Push-ListGraphRequestQueue { Data = [string]$Json } Add-CIPPAzDataTableEntity @Table -Entity $GraphResults -Force | Out-Null + + if ($env:CIPPNG -eq 'true') { + try { + [Craft.Services.CacheBridge]::InvalidateByScope('AllTenants') + } catch { + Write-Information "CacheBridge invalidation skipped: $($_.Exception.Message)" + } + } + return $true } catch { Write-Warning "Queue Error: $($_.Exception.Message)" diff --git a/Modules/CIPPCore/Public/CippQueue/New-CippQueueEntry.ps1 b/Modules/CIPPCore/Public/CippQueue/New-CippQueueEntry.ps1 index 6302f05994e8..ae77578f4ae3 100644 --- a/Modules/CIPPCore/Public/CippQueue/New-CippQueueEntry.ps1 +++ b/Modules/CIPPCore/Public/CippQueue/New-CippQueueEntry.ps1 @@ -21,6 +21,7 @@ function New-CippQueueEntry { } if ($env:CIPPNG -eq 'true') { + [Craft.Services.QueueStatusBridge]::RegisterQueueMetadata($QueueEntry.RowKey, $Name, $Link, $Reference) return $QueueEntry } diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPOrchestrator.ps1 index 31a13b68a50b..b5509bb4efc4 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPOrchestrator.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPOrchestrator.ps1 @@ -42,10 +42,7 @@ function Start-CIPPOrchestrator { if (-not $InputObject.Batch -and $InputObject.QueueFunction) { $QueueFuncName = "Push-$($InputObject.QueueFunction.FunctionName)" Write-Information "Craft: Calling QueueFunction '$QueueFuncName' to build batch for '$OrchestratorName'" - $QueueItem = [PSCustomObject]@{} - if ($InputObject.QueueFunction.Parameters) { - $QueueItem = [PSCustomObject]$InputObject.QueueFunction.Parameters - } + $QueueItem = [PSCustomObject]$InputObject.QueueFunction $BatchResult = & $QueueFuncName -Item $QueueItem $QueueBatch = @($BatchResult | Where-Object { $null -ne $_ }) if ($QueueBatch.Count -eq 0) { @@ -78,7 +75,8 @@ function Start-CIPPOrchestrator { $BatchJson, 4, $PostExecFunctionName, - $PostExecParametersJson + $PostExecParametersJson, + $InputObject.Reference ) return "Craft-$OrchestratorName" } diff --git a/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 b/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 index ad1db76305bb..134f8aeeeb79 100644 --- a/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 +++ b/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 @@ -199,7 +199,7 @@ function Get-GraphRequestList { $Type = 'Queue' Write-Information "Cached: $(($Rows | Measure-Object).Count) rows (Type: $($Type))" $QueueReference = '{0}-{1}' -f $TenantFilter, $PartitionKey - $RunningQueue = Get-CIPPQueueData -Reference $QueueReference | Where-Object { $_.Status -ne 'Completed' -and $_.Status -ne 'Failed' } + $RunningQueue = Get-CIPPQueueData -Reference $QueueReference | Where-Object { $_.Status -ne 'Completed' -and $_.Status -ne 'Failed' -and $_.Reference -eq $QueueReference } } elseif (!$SkipCache.IsPresent -and !$ClearCache.IsPresent -and !$CountOnly.IsPresent) { if ($TenantFilter -eq 'AllTenants' -or $Count -gt $SingleTenantThreshold) { $Table = Get-CIPPTable -TableName $TableName @@ -214,7 +214,7 @@ function Get-GraphRequestList { $Type = 'Cache' Write-Information "Table: $TableName | PK: $PartitionKey | Cached: $(($Rows | Measure-Object).Count) rows (Type: $($Type))" $QueueReference = '{0}-{1}' -f $TenantFilter, $PartitionKey - $RunningQueue = Get-CIPPQueueData -Reference $QueueReference | Where-Object { $_.Status -notmatch 'Completed' -and $_.Status -notmatch 'Failed' } + $RunningQueue = Get-CIPPQueueData -Reference $QueueReference | Where-Object { $_.Status -notmatch 'Completed' -and $_.Status -notmatch 'Failed' -and $_.Reference -eq $QueueReference } } } } catch { @@ -294,6 +294,7 @@ function Get-GraphRequestList { $InputObject = @{ OrchestratorName = 'GraphRequestOrchestrator' Batch = @($Batch) + Reference = $QueueReference } #Write-Information ($InputObject | ConvertTo-Json -Depth 5) $InstanceId = Start-CIPPOrchestrator -InputObject $InputObject From 8e7392da6e5a9bf03c4c5b9ea1c2f0d1a9bc1fa7 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 7 May 2026 14:51:47 -0400 Subject: [PATCH 09/36] fix: scripted alert optimization --- .../Get-CIPPAlertIntunePolicyConflicts.ps1 | 59 ++++++++++--------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertIntunePolicyConflicts.ps1 b/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertIntunePolicyConflicts.ps1 index d9b2239b1bcb..071f65cff4e7 100644 --- a/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertIntunePolicyConflicts.ps1 +++ b/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertIntunePolicyConflicts.ps1 @@ -48,9 +48,10 @@ function Get-CIPPAlertIntunePolicyConflicts { return } - $AlertableStatuses = @() - if ($Config.AlertErrors) { $AlertableStatuses += 'error', 'failed' } - if ($Config.AlertConflicts) { $AlertableStatuses += 'conflict' } + $AlertableStatuses = @( + if ($Config.AlertErrors) { 'error'; 'failed' } + if ($Config.AlertConflicts) { 'conflict' } + ) if (-not $AlertableStatuses) { return @@ -68,7 +69,7 @@ function Get-CIPPAlertIntunePolicyConflicts { return } - $Issues = @() + $Issues = [System.Collections.Generic.List[object]]::new() if ($Config.IncludePolicies) { try { @@ -77,16 +78,16 @@ function Get-CIPPAlertIntunePolicyConflicts { foreach ($Device in $ManagedDevices) { $PolicyStates = $Device.deviceConfigurationStates | Where-Object { $_.state -and ($AlertableStatuses -contains $_.state) } foreach ($State in $PolicyStates) { - $Issues += [PSCustomObject]@{ - Message = "Policy '$($State.displayName)' is $($State.state) on device '$($Device.deviceName)' for $($Device.userPrincipalName)." - Tenant = $TenantFilter - Type = 'Policy' - PolicyName = $State.displayName - IssueStatus = $State.state - DeviceName = $Device.deviceName - UserPrincipalName = $Device.userPrincipalName - DeviceId = $Device.id - } + $Issues.Add([PSCustomObject]@{ + Message = "Policy '$($State.displayName)' is $($State.state) on device '$($Device.deviceName)' for $($Device.userPrincipalName)." + Tenant = $TenantFilter + Type = 'Policy' + PolicyName = $State.displayName + IssueStatus = $State.state + DeviceName = $Device.deviceName + UserPrincipalName = $Device.userPrincipalName + DeviceId = $Device.id + }) } } } catch { @@ -105,16 +106,16 @@ function Get-CIPPAlertIntunePolicyConflicts { } foreach ($Status in $BadStatuses) { - $Issues += [PSCustomObject]@{ - Message = "App '$($App.displayName)' install is $($Status.installState) on device '$($Status.deviceName)' for $($Status.userPrincipalName)." - Tenant = $TenantFilter - Type = 'Application' - AppName = $App.displayName - IssueStatus = $Status.installState - DeviceName = $Status.deviceName - UserPrincipalName = $Status.userPrincipalName - DeviceId = $Status.deviceId - } + $Issues.Add([PSCustomObject]@{ + Message = "App '$($App.displayName)' install is $($Status.installState) on device '$($Status.deviceName)' for $($Status.userPrincipalName)." + Tenant = $TenantFilter + Type = 'Application' + AppName = $App.displayName + IssueStatus = $Status.installState + DeviceName = $Status.deviceName + UserPrincipalName = $Status.userPrincipalName + DeviceId = $Status.deviceId + }) } } } catch { @@ -132,11 +133,11 @@ function Get-CIPPAlertIntunePolicyConflicts { $AppCount = ($Issues | Where-Object { $_.Type -eq 'Application' }).Count $AlertData = @([PSCustomObject]@{ - Message = "Found $PolicyCount policy issues and $AppCount application issues in Intune." - Tenant = $TenantFilter - PolicyIssues = $PolicyCount - AppIssues = $AppCount - Issues = $Issues + Message = "Found $PolicyCount policy issues and $AppCount application issues in Intune." + Tenant = $TenantFilter + PolicyIssues = $PolicyCount + AppIssues = $AppCount + Issues = $Issues }) } else { $AlertData = $Issues From 731a41edf5e1c80bb5d1b1bfacd095022fd746bf Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 7 May 2026 15:33:22 -0400 Subject: [PATCH 10/36] fix: ensure unique and non-null email addresses in report generation --- .../Public/Standards/Invoke-CIPPStandardMailContacts.ps1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardMailContacts.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardMailContacts.ps1 index a8036b8ea184..0b27c0a250b3 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardMailContacts.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardMailContacts.ps1 @@ -106,13 +106,13 @@ function Invoke-CIPPStandardMailContacts { } if ($Settings.report -eq $true) { $CurrentValue = @{ - marketingNotificationEmails = @($CurrentInfo.marketingNotificationEmails | Sort-Object) - technicalNotificationMails = @($CurrentInfo.technicalNotificationMails | Sort-Object) + marketingNotificationEmails = @($CurrentInfo.marketingNotificationEmails | Sort-Object -Unique) + technicalNotificationMails = @($CurrentInfo.technicalNotificationMails | Where-Object { [string]::IsNullOrWhiteSpace($_) -eq $false } | Sort-Object -Unique) contactEmail = $CurrentInfo.privacyProfile.contactEmail } $ExpectedValue = @{ - marketingNotificationEmails = @($Contacts.MarketingContact | Sort-Object) - technicalNotificationMails = @(@($Contacts.SecurityContact, $Contacts.TechContact) | Where-Object { $_ -ne $null } | Select-Object -Unique | Sort-Object) + marketingNotificationEmails = @($Contacts.MarketingContact | Sort-Object -Unique) + technicalNotificationMails = @(@($Contacts.SecurityContact, $Contacts.TechContact) | Where-Object { [string]::IsNullOrWhiteSpace($_) -eq $false } | Sort-Object -Unique) contactEmail = $Contacts.GeneralContact } Set-CIPPStandardsCompareField -FieldName 'standards.MailContacts' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $tenant From bc4e643dbd7a65030681b89e758733fb1b89dad6 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 8 May 2026 00:19:26 +0200 Subject: [PATCH 11/36] add purview section --- .../Invoke-AddDlpCompliancePolicy.ps1 | 48 ++++++++++++++++++ .../Invoke-AddDlpCompliancePolicyTemplate.ps1 | 49 +++++++++++++++++++ .../Invoke-EditDlpCompliancePolicy.ps1 | 48 ++++++++++++++++++ .../Invoke-ListDlpCompliancePolicy.ps1 | 30 ++++++++++++ ...nvoke-ListDlpCompliancePolicyTemplates.ps1 | 26 ++++++++++ .../Invoke-RemoveDlpCompliancePolicy.ps1 | 36 ++++++++++++++ ...voke-RemoveDlpCompliancePolicyTemplate.ps1 | 36 ++++++++++++++ .../Invoke-AddSensitiveInfoType.ps1 | 47 ++++++++++++++++++ .../Invoke-AddSensitiveInfoTypeTemplate.ps1 | 48 ++++++++++++++++++ .../Invoke-EditSensitiveInfoType.ps1 | 41 ++++++++++++++++ .../Invoke-ListSensitiveInfoType.ps1 | 33 +++++++++++++ .../Invoke-ListSensitiveInfoTypeTemplates.ps1 | 26 ++++++++++ .../Invoke-RemoveSensitiveInfoType.ps1 | 36 ++++++++++++++ ...Invoke-RemoveSensitiveInfoTypeTemplate.ps1 | 36 ++++++++++++++ .../Invoke-AddSensitivityLabel.ps1 | 49 +++++++++++++++++++ .../Invoke-AddSensitivityLabelTemplate.ps1 | 48 ++++++++++++++++++ .../Invoke-EditSensitivityLabel.ps1 | 41 ++++++++++++++++ .../Invoke-ListSensitivityLabel.ps1 | 35 +++++++++++++ .../Invoke-ListSensitivityLabelTemplates.ps1 | 26 ++++++++++ .../Invoke-RemoveSensitivityLabel.ps1 | 36 ++++++++++++++ .../Invoke-RemoveSensitivityLabelTemplate.ps1 | 36 ++++++++++++++ 21 files changed, 811 insertions(+) create mode 100644 Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-AddDlpCompliancePolicy.ps1 create mode 100644 Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-AddDlpCompliancePolicyTemplate.ps1 create mode 100644 Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-EditDlpCompliancePolicy.ps1 create mode 100644 Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-ListDlpCompliancePolicy.ps1 create mode 100644 Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-ListDlpCompliancePolicyTemplates.ps1 create mode 100644 Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-RemoveDlpCompliancePolicy.ps1 create mode 100644 Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-RemoveDlpCompliancePolicyTemplate.ps1 create mode 100644 Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-AddSensitiveInfoType.ps1 create mode 100644 Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-AddSensitiveInfoTypeTemplate.ps1 create mode 100644 Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-EditSensitiveInfoType.ps1 create mode 100644 Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-ListSensitiveInfoType.ps1 create mode 100644 Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-ListSensitiveInfoTypeTemplates.ps1 create mode 100644 Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-RemoveSensitiveInfoType.ps1 create mode 100644 Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-RemoveSensitiveInfoTypeTemplate.ps1 create mode 100644 Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-AddSensitivityLabel.ps1 create mode 100644 Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-AddSensitivityLabelTemplate.ps1 create mode 100644 Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-EditSensitivityLabel.ps1 create mode 100644 Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-ListSensitivityLabel.ps1 create mode 100644 Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-ListSensitivityLabelTemplates.ps1 create mode 100644 Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-RemoveSensitivityLabel.ps1 create mode 100644 Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-RemoveSensitivityLabelTemplate.ps1 diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-AddDlpCompliancePolicy.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-AddDlpCompliancePolicy.ps1 new file mode 100644 index 000000000000..c251373790d7 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-AddDlpCompliancePolicy.ps1 @@ -0,0 +1,48 @@ +Function Invoke-AddDlpCompliancePolicy { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.DlpCompliancePolicy.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + $RequestParams = $Request.Body.PowerShellCommand | ConvertFrom-Json | Select-Object -Property * -ExcludeProperty GUID, comments, RuleParams + $RuleParams = ($Request.Body.PowerShellCommand | ConvertFrom-Json).RuleParams + + $Tenants = ($Request.Body.selectedTenants).value + $Result = foreach ($TenantFilter in $Tenants) { + try { + $PolicyParams = $RequestParams | Select-Object -Property * -ExcludeProperty RuleParams + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'New-DlpCompliancePolicy' -cmdParams $PolicyParams -Compliance -useSystemMailbox $true + + if ($RuleParams) { + # Ensure rule references the new policy + $RuleHash = @{} + $RuleParams.PSObject.Properties | ForEach-Object { $RuleHash[$_.Name] = $_.Value } + $RuleHash['Policy'] = $RequestParams.Name + if (-not $RuleHash.ContainsKey('Name') -or [string]::IsNullOrWhiteSpace($RuleHash['Name'])) { + $RuleHash['Name'] = "$($RequestParams.Name) Rule" + } + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'New-DlpComplianceRule' -cmdParams $RuleHash -Compliance -useSystemMailbox $true + } + + "Successfully created DLP compliance policy $($RequestParams.Name) for $TenantFilter." + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Successfully created DLP compliance policy $($RequestParams.Name) for $TenantFilter." -sev Info + } catch { + $ErrorMessage = Get-CippException -Exception $_ + "Could not create DLP compliance policy for $($TenantFilter): $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Could not create DLP compliance policy for $($TenantFilter): $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + } + } + + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = @{Results = @($Result) } + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-AddDlpCompliancePolicyTemplate.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-AddDlpCompliancePolicyTemplate.ps1 new file mode 100644 index 000000000000..911c7817935f --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-AddDlpCompliancePolicyTemplate.ps1 @@ -0,0 +1,49 @@ +Function Invoke-AddDlpCompliancePolicyTemplate { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.DlpCompliancePolicy.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + try { + $GUID = (New-Guid).GUID + $JSON = if ($Request.Body.PowerShellCommand) { + $Request.Body.PowerShellCommand | ConvertFrom-Json + } else { + ([pscustomobject]$Request.Body | Select-Object Name, Comment, Mode, Workload, Enabled, ExchangeLocation, ExchangeSenderMemberOf, ExchangeSenderMemberOfException, SharePointLocation, SharePointLocationException, OneDriveLocation, OneDriveLocationException, TeamsLocation, TeamsLocationException, EndpointDlpLocation, EndpointDlpLocationException, OnPremisesScannerDlpLocation, OnPremisesScannerDlpLocationException, ThirdPartyAppDlpLocation, ThirdPartyAppDlpLocationException, PowerBIDlpLocation, PowerBIDlpLocationException, RuleParams) | ForEach-Object { + $NonEmptyProperties = $_.PSObject.Properties | Where-Object { $null -ne $_.Value } | Select-Object -ExpandProperty Name + $_ | Select-Object -Property $NonEmptyProperties + } + } + + # Allow Name to be sourced from displayName/name fields and ensure templated comments preserved + $JSON = ($JSON | Select-Object @{n = 'name'; e = { $_.Name ?? $_.name } }, @{n = 'comments'; e = { $_.Comment ?? $_.comments } }, * | ConvertTo-Json -Depth 10) + $Table = Get-CippTable -tablename 'templates' + $Table.Force = $true + Add-CIPPAzDataTableEntity @Table -Entity @{ + JSON = "$JSON" + RowKey = "$GUID" + PartitionKey = 'DlpCompliancePolicyTemplate' + } + $Result = "Successfully created DLP Compliance Policy Template: $($Request.Body.Name ?? $Request.Body.name) with GUID $GUID" + Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Debug' + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to create DLP Compliance Policy Template: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Error' -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::InternalServerError + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{Results = $Result } + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-EditDlpCompliancePolicy.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-EditDlpCompliancePolicy.ps1 new file mode 100644 index 000000000000..c0e8daee0627 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-EditDlpCompliancePolicy.ps1 @@ -0,0 +1,48 @@ +Function Invoke-EditDlpCompliancePolicy { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.DlpCompliancePolicy.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter + $Identity = $Request.Query.Identity ?? $Request.Body.Identity ?? $Request.Body.Name + $State = $Request.Query.State ?? $Request.Body.State + + try { + $Params = @{ + Identity = $Identity + } + + # If a state was passed, toggle Enabled on the policy + if ($State) { + $Params['Enabled'] = ($State -eq 'enable' -or $State -eq $true -or $State -eq 'true') + } + + # Allow passing arbitrary additional params via Body.parameters + if ($Request.Body.parameters) { + $Request.Body.parameters.PSObject.Properties | ForEach-Object { + if ($_.Name -ne 'Identity') { $Params[$_.Name] = $_.Value } + } + } + + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Set-DlpCompliancePolicy' -cmdParams $Params -Compliance -useSystemMailbox $true + $Result = "Updated DLP compliance policy $Identity" + Write-LogMessage -Headers $Request.Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Info + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed updating DLP compliance policy $Identity. Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -Headers $Request.Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Error -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::Forbidden + } + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{Results = $Result } + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-ListDlpCompliancePolicy.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-ListDlpCompliancePolicy.ps1 new file mode 100644 index 000000000000..26fcd8eafb9e --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-ListDlpCompliancePolicy.ps1 @@ -0,0 +1,30 @@ +Function Invoke-ListDlpCompliancePolicy { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.DlpCompliancePolicy.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + $TenantFilter = $Request.Query.tenantFilter + + try { + $Policies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-DlpCompliancePolicy' -Compliance | Select-Object * -ExcludeProperty *odata*, *data.type* + $Rules = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-DlpComplianceRule' -Compliance | Select-Object * -ExcludeProperty *odata*, *data.type* + $GraphRequest = $Policies | Select-Object *, + @{l = 'AssociatedRules'; e = { $name = $_.Name; @($Rules | Where-Object { $_.ParentPolicyName -eq $name }) } }, + @{l = 'RuleCount'; e = { $name = $_.Name; (@($Rules | Where-Object { $_.ParentPolicyName -eq $name })).Count } } + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + $StatusCode = [HttpStatusCode]::Forbidden + $GraphRequest = $ErrorMessage + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @($GraphRequest) + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-ListDlpCompliancePolicyTemplates.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-ListDlpCompliancePolicyTemplates.ps1 new file mode 100644 index 000000000000..a958e9acd2ee --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-ListDlpCompliancePolicyTemplates.ps1 @@ -0,0 +1,26 @@ +Function Invoke-ListDlpCompliancePolicyTemplates { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + Security.DlpCompliancePolicy.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + $Table = Get-CippTable -tablename 'templates' + $Filter = "PartitionKey eq 'DlpCompliancePolicyTemplate'" + $Templates = (Get-CIPPAzDataTableEntity @Table -Filter $Filter) | ForEach-Object { + $GUID = $_.RowKey + $data = $_.JSON | ConvertFrom-Json + $data | Add-Member -NotePropertyName 'GUID' -NotePropertyValue $GUID -Force + $data + } + + if ($Request.Query.ID) { $Templates = $Templates | Where-Object -Property GUID -EQ $Request.Query.ID } + + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = @($Templates) + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-RemoveDlpCompliancePolicy.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-RemoveDlpCompliancePolicy.ps1 new file mode 100644 index 000000000000..4af00a520304 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-RemoveDlpCompliancePolicy.ps1 @@ -0,0 +1,36 @@ +Function Invoke-RemoveDlpCompliancePolicy { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.DlpCompliancePolicy.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter + $Identity = $Request.Query.Identity ?? $Request.Body.Identity ?? $Request.Body.Name + + try { + $Params = @{ + Identity = $Identity + } + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Remove-DlpCompliancePolicy' -cmdParams $Params -Compliance -useSystemMailbox $true + $Result = "Deleted DLP compliance policy $Identity" + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Info + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to delete DLP compliance policy $Identity - $($ErrorMessage.NormalizedError)" + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Error -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::Forbidden + } + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{Results = $Result } + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-RemoveDlpCompliancePolicyTemplate.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-RemoveDlpCompliancePolicyTemplate.ps1 new file mode 100644 index 000000000000..9504141526e5 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-RemoveDlpCompliancePolicyTemplate.ps1 @@ -0,0 +1,36 @@ +Function Invoke-RemoveDlpCompliancePolicyTemplate { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.DlpCompliancePolicy.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + $ID = $Request.Body.ID ?? $Request.Query.ID + try { + $Table = Get-CippTable -tablename 'templates' + $SafeID = ConvertTo-CIPPODataFilterValue -Value $ID -Type Guid + $Filter = "PartitionKey eq 'DlpCompliancePolicyTemplate' and RowKey eq '$SafeID'" + $ClearRow = Get-CIPPAzDataTableEntity @Table -Filter $Filter -Property PartitionKey, RowKey + Remove-AzDataTableEntity -Force @Table -Entity $ClearRow + $Result = "Removed DLP Compliance Policy template with ID $ID" + Write-LogMessage -Headers $Headers -API $APIName -message $Result -Sev 'Info' + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to remove DLP Compliance Policy template $ID. $($ErrorMessage.NormalizedError)" + Write-LogMessage -Headers $Headers -API $APIName -message $Result -Sev 'Error' -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::Forbidden + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{'Results' = $Result } + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-AddSensitiveInfoType.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-AddSensitiveInfoType.ps1 new file mode 100644 index 000000000000..b8d530757c05 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-AddSensitiveInfoType.ps1 @@ -0,0 +1,47 @@ +Function Invoke-AddSensitiveInfoType { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.SensitiveInfoType.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + $RequestParams = $Request.Body.PowerShellCommand | ConvertFrom-Json | Select-Object -Property * -ExcludeProperty GUID, comments + + $Tenants = ($Request.Body.selectedTenants).value + $Result = foreach ($TenantFilter in $Tenants) { + try { + # New-DlpSensitiveInformationType expects FileData (byte array of XML rule pack) or specific simple parameters. + # We pass through whatever the user provided as JSON parameters. + $Params = @{} + $RequestParams.PSObject.Properties | ForEach-Object { + $Params[$_.Name] = $_.Value + } + + # If the template provided XML rule pack content as base64, decode it for FileData + if ($Params.ContainsKey('FileDataBase64') -and $Params['FileDataBase64']) { + $Params['FileData'] = [System.Convert]::FromBase64String($Params['FileDataBase64']) + $Params.Remove('FileDataBase64') + } + + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'New-DlpSensitiveInformationType' -cmdParams $Params -Compliance -useSystemMailbox $true + "Successfully created Sensitive Information Type $($RequestParams.Name) for $TenantFilter." + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Successfully created Sensitive Information Type $($RequestParams.Name) for $TenantFilter." -sev Info + } catch { + $ErrorMessage = Get-CippException -Exception $_ + "Could not create Sensitive Information Type for $($TenantFilter): $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Could not create Sensitive Information Type for $($TenantFilter): $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + } + } + + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = @{Results = @($Result) } + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-AddSensitiveInfoTypeTemplate.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-AddSensitiveInfoTypeTemplate.ps1 new file mode 100644 index 000000000000..d4f18b501fb0 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-AddSensitiveInfoTypeTemplate.ps1 @@ -0,0 +1,48 @@ +Function Invoke-AddSensitiveInfoTypeTemplate { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.SensitiveInfoType.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + try { + $GUID = (New-Guid).GUID + $JSON = if ($Request.Body.PowerShellCommand) { + $Request.Body.PowerShellCommand | ConvertFrom-Json + } else { + ([pscustomobject]$Request.Body | Select-Object Name, Description, Pattern, FileDataBase64, Locale, Publisher, Confidence, Recommended, RulePackVersion, Comment) | ForEach-Object { + $NonEmptyProperties = $_.PSObject.Properties | Where-Object { $null -ne $_.Value } | Select-Object -ExpandProperty Name + $_ | Select-Object -Property $NonEmptyProperties + } + } + + $JSON = ($JSON | Select-Object @{n = 'name'; e = { $_.Name ?? $_.name } }, @{n = 'comments'; e = { $_.Comment ?? $_.comments } }, * | ConvertTo-Json -Depth 10) + $Table = Get-CippTable -tablename 'templates' + $Table.Force = $true + Add-CIPPAzDataTableEntity @Table -Entity @{ + JSON = "$JSON" + RowKey = "$GUID" + PartitionKey = 'SensitiveInfoTypeTemplate' + } + $Result = "Successfully created Sensitive Information Type Template: $($Request.Body.Name ?? $Request.Body.name) with GUID $GUID" + Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Debug' + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to create Sensitive Information Type Template: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Error' -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::InternalServerError + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{Results = $Result } + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-EditSensitiveInfoType.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-EditSensitiveInfoType.ps1 new file mode 100644 index 000000000000..213ac25966fa --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-EditSensitiveInfoType.ps1 @@ -0,0 +1,41 @@ +Function Invoke-EditSensitiveInfoType { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.SensitiveInfoType.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter + $Identity = $Request.Query.Identity ?? $Request.Body.Identity ?? $Request.Body.Name + + try { + $Params = @{ + Identity = $Identity + } + + if ($Request.Body.parameters) { + $Request.Body.parameters.PSObject.Properties | ForEach-Object { + if ($_.Name -ne 'Identity') { $Params[$_.Name] = $_.Value } + } + } + + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Set-DlpSensitiveInformationType' -cmdParams $Params -Compliance -useSystemMailbox $true + $Result = "Updated Sensitive Information Type $Identity" + Write-LogMessage -Headers $Request.Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Info + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed updating Sensitive Information Type $Identity. Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -Headers $Request.Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Error -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::Forbidden + } + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{Results = $Result } + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-ListSensitiveInfoType.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-ListSensitiveInfoType.ps1 new file mode 100644 index 000000000000..5ae4a340fe49 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-ListSensitiveInfoType.ps1 @@ -0,0 +1,33 @@ +Function Invoke-ListSensitiveInfoType { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.SensitiveInfoType.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + $TenantFilter = $Request.Query.tenantFilter + $IncludeBuiltIn = ($Request.Query.IncludeBuiltIn -eq 'true' -or $Request.Query.IncludeBuiltIn -eq $true) + + try { + $SITs = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-DlpSensitiveInformationType' -Compliance | Select-Object * -ExcludeProperty *odata*, *data.type* + + if (-not $IncludeBuiltIn) { + $SITs = $SITs | Where-Object { $_.Publisher -ne 'Microsoft Corporation' -and $_.Publisher -notlike 'Microsoft*' } + } + + $StatusCode = [HttpStatusCode]::OK + $GraphRequest = $SITs + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + $StatusCode = [HttpStatusCode]::Forbidden + $GraphRequest = $ErrorMessage + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @($GraphRequest) + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-ListSensitiveInfoTypeTemplates.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-ListSensitiveInfoTypeTemplates.ps1 new file mode 100644 index 000000000000..fa21108ce653 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-ListSensitiveInfoTypeTemplates.ps1 @@ -0,0 +1,26 @@ +Function Invoke-ListSensitiveInfoTypeTemplates { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + Security.SensitiveInfoType.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + $Table = Get-CippTable -tablename 'templates' + $Filter = "PartitionKey eq 'SensitiveInfoTypeTemplate'" + $Templates = (Get-CIPPAzDataTableEntity @Table -Filter $Filter) | ForEach-Object { + $GUID = $_.RowKey + $data = $_.JSON | ConvertFrom-Json + $data | Add-Member -NotePropertyName 'GUID' -NotePropertyValue $GUID -Force + $data + } + + if ($Request.Query.ID) { $Templates = $Templates | Where-Object -Property GUID -EQ $Request.Query.ID } + + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = @($Templates) + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-RemoveSensitiveInfoType.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-RemoveSensitiveInfoType.ps1 new file mode 100644 index 000000000000..3fa5502ee294 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-RemoveSensitiveInfoType.ps1 @@ -0,0 +1,36 @@ +Function Invoke-RemoveSensitiveInfoType { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.SensitiveInfoType.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter + $Identity = $Request.Query.Identity ?? $Request.Body.Identity ?? $Request.Body.Name + + try { + $Params = @{ + Identity = $Identity + } + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Remove-DlpSensitiveInformationType' -cmdParams $Params -Compliance -useSystemMailbox $true + $Result = "Deleted Sensitive Information Type $Identity" + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Info + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to delete Sensitive Information Type $Identity - $($ErrorMessage.NormalizedError)" + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Error -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::Forbidden + } + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{Results = $Result } + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-RemoveSensitiveInfoTypeTemplate.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-RemoveSensitiveInfoTypeTemplate.ps1 new file mode 100644 index 000000000000..23c88a9572a6 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-RemoveSensitiveInfoTypeTemplate.ps1 @@ -0,0 +1,36 @@ +Function Invoke-RemoveSensitiveInfoTypeTemplate { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.SensitiveInfoType.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + $ID = $Request.Body.ID ?? $Request.Query.ID + try { + $Table = Get-CippTable -tablename 'templates' + $SafeID = ConvertTo-CIPPODataFilterValue -Value $ID -Type Guid + $Filter = "PartitionKey eq 'SensitiveInfoTypeTemplate' and RowKey eq '$SafeID'" + $ClearRow = Get-CIPPAzDataTableEntity @Table -Filter $Filter -Property PartitionKey, RowKey + Remove-AzDataTableEntity -Force @Table -Entity $ClearRow + $Result = "Removed Sensitive Information Type template with ID $ID" + Write-LogMessage -Headers $Headers -API $APIName -message $Result -Sev 'Info' + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to remove Sensitive Information Type template $ID. $($ErrorMessage.NormalizedError)" + Write-LogMessage -Headers $Headers -API $APIName -message $Result -Sev 'Error' -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::Forbidden + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{'Results' = $Result } + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-AddSensitivityLabel.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-AddSensitivityLabel.ps1 new file mode 100644 index 000000000000..9f47b293933f --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-AddSensitivityLabel.ps1 @@ -0,0 +1,49 @@ +Function Invoke-AddSensitivityLabel { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.SensitivityLabel.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + $RequestParams = $Request.Body.PowerShellCommand | ConvertFrom-Json | Select-Object -Property * -ExcludeProperty GUID, comments, PolicyParams + $PolicyParams = ($Request.Body.PowerShellCommand | ConvertFrom-Json).PolicyParams + + $Tenants = ($Request.Body.selectedTenants).value + $Result = foreach ($TenantFilter in $Tenants) { + try { + $LabelParams = $RequestParams | Select-Object -Property * -ExcludeProperty PolicyParams + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'New-Label' -cmdParams $LabelParams -Compliance -useSystemMailbox $true + + if ($PolicyParams) { + $PolicyHash = @{} + $PolicyParams.PSObject.Properties | ForEach-Object { $PolicyHash[$_.Name] = $_.Value } + if (-not $PolicyHash.ContainsKey('Labels') -or -not $PolicyHash['Labels']) { + $PolicyHash['Labels'] = @($RequestParams.Name) + } + if (-not $PolicyHash.ContainsKey('Name') -or [string]::IsNullOrWhiteSpace($PolicyHash['Name'])) { + $PolicyHash['Name'] = "$($RequestParams.Name) Policy" + } + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'New-LabelPolicy' -cmdParams $PolicyHash -Compliance -useSystemMailbox $true + } + + "Successfully created sensitivity label $($RequestParams.Name) for $TenantFilter." + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Successfully created sensitivity label $($RequestParams.Name) for $TenantFilter." -sev Info + } catch { + $ErrorMessage = Get-CippException -Exception $_ + "Could not create sensitivity label for $($TenantFilter): $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Could not create sensitivity label for $($TenantFilter): $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + } + } + + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = @{Results = @($Result) } + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-AddSensitivityLabelTemplate.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-AddSensitivityLabelTemplate.ps1 new file mode 100644 index 000000000000..045a31fa3d80 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-AddSensitivityLabelTemplate.ps1 @@ -0,0 +1,48 @@ +Function Invoke-AddSensitivityLabelTemplate { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.SensitivityLabel.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + try { + $GUID = (New-Guid).GUID + $JSON = if ($Request.Body.PowerShellCommand) { + $Request.Body.PowerShellCommand | ConvertFrom-Json + } else { + ([pscustomobject]$Request.Body | Select-Object Name, DisplayName, Comment, Tooltip, ParentId, ContentType, EncryptionEnabled, EncryptionProtectionType, EncryptionRightsDefinitions, EncryptionContentExpiredOnDateInDaysOrNever, EncryptionDoNotForward, EncryptionEncryptOnly, EncryptionOfflineAccessDays, EncryptionPromptUser, EncryptionAESKeySize, ContentMarkingHeaderEnabled, ContentMarkingHeaderText, ContentMarkingHeaderFontSize, ContentMarkingHeaderFontColor, ContentMarkingHeaderAlignment, ContentMarkingFooterEnabled, ContentMarkingFooterText, ContentMarkingFooterFontSize, ContentMarkingFooterFontColor, ContentMarkingFooterAlignment, ContentMarkingWatermarkEnabled, ContentMarkingWatermarkText, ContentMarkingWatermarkFontSize, ContentMarkingWatermarkFontColor, ContentMarkingWatermarkLayout, ApplyContentMarkingHeaderEnabled, ApplyContentMarkingFooterEnabled, ApplyWaterMarkingEnabled, SiteAndGroupProtectionEnabled, SiteAndGroupProtectionPrivacy, SiteAndGroupProtectionAllowAccessToGuestUsers, SiteAndGroupProtectionAllowEmailFromGuestUsers, SiteAndGroupProtectionAllowFullAccess, SiteAndGroupProtectionAllowLimitedAccess, SiteAndGroupProtectionBlockAccess, Conditions, AdvancedSettings, PolicyParams) | ForEach-Object { + $NonEmptyProperties = $_.PSObject.Properties | Where-Object { $null -ne $_.Value } | Select-Object -ExpandProperty Name + $_ | Select-Object -Property $NonEmptyProperties + } + } + + $JSON = ($JSON | Select-Object @{n = 'name'; e = { $_.Name ?? $_.name } }, @{n = 'comments'; e = { $_.Comment ?? $_.comments } }, * | ConvertTo-Json -Depth 10) + $Table = Get-CippTable -tablename 'templates' + $Table.Force = $true + Add-CIPPAzDataTableEntity @Table -Entity @{ + JSON = "$JSON" + RowKey = "$GUID" + PartitionKey = 'SensitivityLabelTemplate' + } + $Result = "Successfully created Sensitivity Label Template: $($Request.Body.Name ?? $Request.Body.name) with GUID $GUID" + Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Debug' + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to create Sensitivity Label Template: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Error' -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::InternalServerError + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{Results = $Result } + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-EditSensitivityLabel.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-EditSensitivityLabel.ps1 new file mode 100644 index 000000000000..acad97a18693 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-EditSensitivityLabel.ps1 @@ -0,0 +1,41 @@ +Function Invoke-EditSensitivityLabel { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.SensitivityLabel.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter + $Identity = $Request.Query.Identity ?? $Request.Body.Identity ?? $Request.Body.Name + + try { + $Params = @{ + Identity = $Identity + } + + if ($Request.Body.parameters) { + $Request.Body.parameters.PSObject.Properties | ForEach-Object { + if ($_.Name -ne 'Identity') { $Params[$_.Name] = $_.Value } + } + } + + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Set-Label' -cmdParams $Params -Compliance -useSystemMailbox $true + $Result = "Updated sensitivity label $Identity" + Write-LogMessage -Headers $Request.Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Info + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed updating sensitivity label $Identity. Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -Headers $Request.Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Error -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::Forbidden + } + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{Results = $Result } + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-ListSensitivityLabel.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-ListSensitivityLabel.ps1 new file mode 100644 index 000000000000..4c6947962223 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-ListSensitivityLabel.ps1 @@ -0,0 +1,35 @@ +Function Invoke-ListSensitivityLabel { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.SensitivityLabel.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + $TenantFilter = $Request.Query.tenantFilter + + try { + $Labels = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-Label' -Compliance | Select-Object * -ExcludeProperty *odata*, *data.type* + $Policies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-LabelPolicy' -Compliance | Select-Object * -ExcludeProperty *odata*, *data.type* + + $GraphRequest = $Labels | Select-Object *, + @{l = 'PublishedInPolicies'; e = { + $labelGuid = $_.Guid + @($Policies | Where-Object { $_.Labels -contains $labelGuid -or $_.Labels -contains $_.ImmutableId }) | Select-Object -ExpandProperty Name + } + } + + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + $StatusCode = [HttpStatusCode]::Forbidden + $GraphRequest = $ErrorMessage + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @($GraphRequest) + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-ListSensitivityLabelTemplates.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-ListSensitivityLabelTemplates.ps1 new file mode 100644 index 000000000000..ab066a4eb6d5 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-ListSensitivityLabelTemplates.ps1 @@ -0,0 +1,26 @@ +Function Invoke-ListSensitivityLabelTemplates { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + Security.SensitivityLabel.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + $Table = Get-CippTable -tablename 'templates' + $Filter = "PartitionKey eq 'SensitivityLabelTemplate'" + $Templates = (Get-CIPPAzDataTableEntity @Table -Filter $Filter) | ForEach-Object { + $GUID = $_.RowKey + $data = $_.JSON | ConvertFrom-Json + $data | Add-Member -NotePropertyName 'GUID' -NotePropertyValue $GUID -Force + $data + } + + if ($Request.Query.ID) { $Templates = $Templates | Where-Object -Property GUID -EQ $Request.Query.ID } + + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = @($Templates) + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-RemoveSensitivityLabel.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-RemoveSensitivityLabel.ps1 new file mode 100644 index 000000000000..225351276dc5 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-RemoveSensitivityLabel.ps1 @@ -0,0 +1,36 @@ +Function Invoke-RemoveSensitivityLabel { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.SensitivityLabel.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter + $Identity = $Request.Query.Identity ?? $Request.Body.Identity ?? $Request.Body.Name + + try { + $Params = @{ + Identity = $Identity + } + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Remove-Label' -cmdParams $Params -Compliance -useSystemMailbox $true + $Result = "Deleted sensitivity label $Identity" + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Info + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to delete sensitivity label $Identity - $($ErrorMessage.NormalizedError)" + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Error -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::Forbidden + } + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{Results = $Result } + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-RemoveSensitivityLabelTemplate.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-RemoveSensitivityLabelTemplate.ps1 new file mode 100644 index 000000000000..c8084265db52 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-RemoveSensitivityLabelTemplate.ps1 @@ -0,0 +1,36 @@ +Function Invoke-RemoveSensitivityLabelTemplate { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.SensitivityLabel.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + $ID = $Request.Body.ID ?? $Request.Query.ID + try { + $Table = Get-CippTable -tablename 'templates' + $SafeID = ConvertTo-CIPPODataFilterValue -Value $ID -Type Guid + $Filter = "PartitionKey eq 'SensitivityLabelTemplate' and RowKey eq '$SafeID'" + $ClearRow = Get-CIPPAzDataTableEntity @Table -Filter $Filter -Property PartitionKey, RowKey + Remove-AzDataTableEntity -Force @Table -Entity $ClearRow + $Result = "Removed Sensitivity Label template with ID $ID" + Write-LogMessage -Headers $Headers -API $APIName -message $Result -Sev 'Info' + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to remove Sensitivity Label template $ID. $($ErrorMessage.NormalizedError)" + Write-LogMessage -Headers $Headers -API $APIName -message $Result -Sev 'Error' -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::Forbidden + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{'Results' = $Result } + }) + +} From c0098fdea73e4ff9a6d5e6be89df0c7a8a18185e Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 8 May 2026 00:46:46 +0200 Subject: [PATCH 12/36] remove old file --- .../Public/Tests/CISA-Missing-Caches.md | 195 ------------------ 1 file changed, 195 deletions(-) delete mode 100644 Modules/CIPPTests/Public/Tests/CISA-Missing-Caches.md diff --git a/Modules/CIPPTests/Public/Tests/CISA-Missing-Caches.md b/Modules/CIPPTests/Public/Tests/CISA-Missing-Caches.md deleted file mode 100644 index 5b7bd652195a..000000000000 --- a/Modules/CIPPTests/Public/Tests/CISA-Missing-Caches.md +++ /dev/null @@ -1,195 +0,0 @@ -# Missing CIPP Caches for CISA Tests - -This document lists the caches that need to be created to support the remaining CISA tests that cannot currently be implemented. - -## ✅ Implemented Cache Functions - -### 1. ✅ CASMailbox Cache -**Required For:** -- ✅ MS.EXO.5.1 - SMTP Authentication - -**Status**: ✅ IMPLEMENTED in Set-CIPPDBCacheCASMailbox.ps1 - ---- - -### 2. ✅ ExoSharingPolicy Cache -**Required For:** -- ✅ MS.EXO.6.1 - Contact Sharing -- ✅ MS.EXO.6.2 - Calendar Sharing - -**Status**: ✅ IMPLEMENTED in Set-CIPPDBCacheExoSharingPolicy.ps1 - ---- - -### 3. ✅ ExoAdminAuditLogConfig Cache -**Required For:** -- ✅ MS.EXO.17.1 - Audit Log -- ✅ MS.EXO.17.3 - Audit Log Retention - -**Status**: ✅ IMPLEMENTED in Set-CIPPDBCacheExoAdminAuditLogConfig.ps1 - ---- - -### 4. ✅ ExoPresetSecurityPolicy Cache -**Required For:** -- ✅ MS.EXO.11.1 - Impersonation -- ✅ MS.EXO.11.2 - Impersonation Tips -- ✅ MS.EXO.11.3 - Mailbox Intelligence - -**Status**: ✅ IMPLEMENTED in Set-CIPPDBCacheExoPresetSecurityPolicy.ps1 - ---- - -### 5. ✅ ExoTenantAllowBlockList Cache -**Required For:** -- ✅ MS.EXO.12.1 - Anti-Spam Allow List - -**Status**: ✅ IMPLEMENTED in Set-CIPPDBCacheExoTenantAllowBlockList.ps1 - ---- - -## Required New Cache Functions -**Required For:** -- MS.EXO.8.1 - DLP Solution -- MS.EXO.8.2 - DLP PII -- MS.EXO.8.4 - DLP Baseline Rules - -**SecurityCompliance Command:** -```powershell -Get-DlpCompliancePolicy | Select-Object Name, Enabled, Mode -Get-DlpComplianceRule | Select-Object Name, ParentPolicyName, ContentContainsSensitiveInformation, Disabled -``` -1 -**Cache Function Names:** -- `Set-CIPPDBCacheSccDlpPolicy` -- `Set-CIPPDBCacheSccDlpRule` - -**Properties Needed:** -- Policy: Name, Enabled, Mode -- Rule: Name, ParentPolicyName, ContentContainsSensitiveInformation, Disabled - -**Note:** Requires SecurityCompliance PowerShell connection - ---- - -### 2. SecurityCompliance ProtectionAlert Cache -**Required For:** -- MS.EXO.16.1 - Alerts - -**SecurityCompliance Command:** -```powershell -Get-ProtectionAlert | Select-Object Name, Disabled -``` - -**Cache Function Name:** `Set-CIPPDBCacheSccProtectionAlert` - -**Properties Needed:** -- Name -- Disabled - -**Note:** Requires SecurityCompliance PowerShell connection - ---- - -### 3. SecurityCompliance ActivityAlert Cache -**Required For:** -- MS.EXO.16.2 - Alert SIEM - -**SecurityCompliance Command:** -```powershell -Get-ActivityAlert | Select-Object Name, Disabled, NotificationEnabled, Type -``` - -**Cache Function Name:** `Set-CIPPDBCacheSccActivityAlert` - -**Properties Needed:** -- Name -- Disabled -- NotificationEnabled -- Type - -**Note:** Requires SecurityCompliance PowerShell connection - ---- - -## DNS-Based Tests (Cannot Be Cached) - -These tests require external DNS lookups and cannot be implemented with cached Exchange data: - -### MS.EXO.2.1 - SPF Restriction -**Requires:** DNS TXT record lookup for SPF -**Query:** `nslookup -type=txt ` - -### MS.EXO.2.2 - SPF Directive -**Requires:** DNS TXT record parsing for SPF policy -**Query:** Parse SPF record for `~all` or `-all` - -### MS.EXO.4.1 - DMARC Record Exists -**Requires:** DNS TXT record lookup for DMARC -**Query:** `nslookup -type=txt _dmarc.` - -### MS.EXO.4.2 - DMARC Reject Policy -**Requires:** DNS TXT record parsing for DMARC policy -**Query:** Parse DMARC record for `p=reject` or `p=quarantine` - -### MS.EXO.4.3 - DMARC Aggregate Reports -**Requires:** DNS TXT record parsing for DMARC rua tags -**Query:** Parse DMARC record for `rua=` email addresses - -### MS.EXO.4.4 - DMARC Reports -**Requires:** DNS TXT record parsing for DMARC report configuration -**Query:** Parse DMARC record for report targets - -### MS.EXO.7.1 - External Sender Warning -**Requires:** ExoOrganizationConfig.ExternalInOutlook property -**Note:** May already be in ExoOrganizationConfig cache - needs verification - -### MS.EXO.13.1 - Mailbox Auditing -**Requires:** ExoOrganizationConfig.AuditDisabled property -**Note:** May already be in ExoOrganizationConfig cache - needs verification - -## Manual Assessment Tests (Cannot Be Automated) - -### MS.EXO.8.3 - DLP Alternate Solution -**Reason:** Requires manual assessment of 3rd party DLP solutions - -### MS.EXO.9.4 - Email Filter Alternative -**Reason:** Requires manual assessment of 3rd party email filtering solutions - -### MS.EXO.14.4 - Spam Alternative Solution -**Reason:** Requires manual assessment of 3rd party anti-spam solutions - -### MS.EXO.17.2 - Audit Log Premium -**Reason:** Requires license validation and advanced audit policy checks beyond cached data - ---- - -## Implementation Priority - -### High Priority (Core Security Controls): -1. CASMailbox - SMTP Auth control -2. ExoAdminAuditLogConfig - Audit logging -3. ExoTenantAllowBlockList - Allow list bypass prevention - -### Medium Priority (DLP and Alerts): -4. SecurityCompliance DLP caches - Data loss prevention -5. SecurityCompliance Alert caches - Security monitoring - -### Low Priority (Advanced Features): -6. ExoSharingPolicy - External sharing controls -7. ExoPresetSecurityPolicy - Preset security policies - ---- - -## Notes on Implementation - -1. **Graph API Alternative**: Some Exchange Online cmdlets may have equivalent Graph API endpoints that could be used instead. - -## Summary - -- **New Caches Required**: 8 cache functions -- **DNS Tests**: 6 tests (architectural limitation) -- **Manual Tests**: 4 tests (cannot be automated) -- **Implementable After New Caches**: 15 additional tests -- **Current Implementation**: 13 tests -- **Total Possible with New Caches**: 28 tests (68% coverage) From 26cb9aee55dcb43b4f5ca97e3f4806f6c7b7662f Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Fri, 8 May 2026 00:50:48 +0200 Subject: [PATCH 13/36] feat: Add AutoDiscover check to domain analysis - Implemented AutoDiscover record validation in Push-DomainAnalyserDomain function. - Enhanced Invoke-ListDomainHealth to support AutoDiscover record retrieval. --- .../Push-DomainAnalyserDomain.ps1 | 21 +++++++++++++++++++ .../Standards/Invoke-ListDomainHealth.ps1 | 9 ++++++++ 2 files changed, 30 insertions(+) diff --git a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-DomainAnalyserDomain.ps1 b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-DomainAnalyserDomain.ps1 index 2a4357ec7d98..508da7a7e02e 100644 --- a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-DomainAnalyserDomain.ps1 +++ b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-DomainAnalyserDomain.ps1 @@ -62,6 +62,7 @@ function Push-DomainAnalyserDomain { MSCNAMEDKIMSelectors = '' EnterpriseEnrollment = '' EnterpriseRegistration = '' + AutoDiscover = '' Score = '' MaximumScore = 160 ScorePercentage = '' @@ -293,6 +294,26 @@ function Push-DomainAnalyserDomain { } #EndRegion Intune Enrollment CNAME Check + #Region AutoDiscover Check + try { + $AutoDiscoverRecord = Read-AutoDiscoverRecord -Domain $Domain + $AutoDiscoverFailCount = $AutoDiscoverRecord.ValidationFails | Measure-Object | Select-Object -ExpandProperty Count + $AutoDiscoverWarnCount = $AutoDiscoverRecord.ValidationWarns | Measure-Object | Select-Object -ExpandProperty Count + if ($AutoDiscoverFailCount -eq 0 -and $AutoDiscoverWarnCount -eq 0) { + $Result.AutoDiscover = 'Correct' + } elseif ($AutoDiscoverFailCount -eq 0) { + $Result.AutoDiscover = "$($AutoDiscoverRecord.RecordType): $($AutoDiscoverRecord.Record)" + $ScoreExplanation.Add("AutoDiscover $($AutoDiscoverRecord.RecordType) record points to unexpected target") | Out-Null + } else { + $Result.AutoDiscover = 'No Record' + $ScoreExplanation.Add('No AutoDiscover DNS record found') | Out-Null + } + } catch { + $Result.AutoDiscover = 'Error' + Write-LogMessage -API 'DomainAnalyser' -tenant $DomainObject.TenantId -message "AutoDiscover check error for $Domain" -LogData (Get-CippException -Exception $_) -sev Error + } + #EndRegion AutoDiscover Check + #Region MSCNAME DKIM Records # Get Microsoft DKIM CNAME selector Records # Ugly, but i needed to create a scope/loop i could break out of without breaking the rest of the function diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListDomainHealth.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListDomainHealth.ps1 index 8a87edb67943..2ae766dda392 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListDomainHealth.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListDomainHealth.ps1 @@ -133,6 +133,15 @@ function Invoke-ListDomainHealth { } $Body = Test-MtaSts @HttpsQuery } + 'ReadAutoDiscover' { + $AutoDiscoverQuery = @{ + Domain = $Request.Query.Domain + } + if ($Request.Query.ExpectedTarget) { + $AutoDiscoverQuery.ExpectedTarget = $Request.Query.ExpectedTarget + } + $Body = Read-AutoDiscoverRecord @AutoDiscoverQuery + } } } else { $body = [pscustomobject]@{'Results' = "Domain: $($Request.Query.Domain) is invalid" } From 94158f4fb0fa4a03a936d2d3726dd27b2e67c06e Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Fri, 8 May 2026 09:12:28 +0800 Subject: [PATCH 14/36] Add Investigate status to custom tests --- .../Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 | 1 + .../Public/Tests/Custom/Invoke-CippTestCustomScripts.ps1 | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 index 981a6b02a64e..4618714eed85 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 @@ -212,6 +212,7 @@ function Invoke-ListTests { Custom = @{ Passed = @($CustomResultsForCounts | Where-Object { $_.Status -eq 'Passed' }).Count Failed = @($CustomResultsForCounts | Where-Object { $_.Status -eq 'Failed' }).Count + NeedsAttention = @($CustomResultsForCounts | Where-Object { $_.Status -eq 'Investigate' }).Count Skipped = @($CustomResultsForCounts | Where-Object { $_.Status -eq 'Skipped' }).Count Informational = @($CustomResultsForCounts | Where-Object { $_.Status -eq 'Informational' }).Count Total = $CustomTotal diff --git a/Modules/CIPPTests/Public/Tests/Custom/Invoke-CippTestCustomScripts.ps1 b/Modules/CIPPTests/Public/Tests/Custom/Invoke-CippTestCustomScripts.ps1 index 75e65e8dc70c..edd5477a2d1e 100644 --- a/Modules/CIPPTests/Public/Tests/Custom/Invoke-CippTestCustomScripts.ps1 +++ b/Modules/CIPPTests/Public/Tests/Custom/Invoke-CippTestCustomScripts.ps1 @@ -68,7 +68,7 @@ function Invoke-CippTestCustomScripts { # Auto-detected status from output, then apply explicit override if present $AutoStatus = if ($FailedRows.Count -gt 0) { 'Failed' } else { 'Passed' } - $ValidExplicitStatuses = @('Passed', 'Failed', 'Info') + $ValidExplicitStatuses = @('Passed', 'Failed', 'Info', 'Investigate') if ($ExplicitStatus -and $ExplicitStatus -in $ValidExplicitStatuses) { $AutoStatus = $ExplicitStatus } @@ -76,6 +76,7 @@ function Invoke-CippTestCustomScripts { $FinalStatus = switch ($ResultMode) { 'AlwaysPass' { 'Passed' } 'AlwaysInfo' { 'Info' } + 'AlwaysInvestigate' { 'Investigate' } default { $AutoStatus } } @@ -90,6 +91,7 @@ function Invoke-CippTestCustomScripts { $FinalStatus = switch ($ResultMode) { 'AlwaysPass' { 'Passed' } 'AlwaysInfo' { 'Info' } + 'AlwaysInvestigate' { 'Investigate' } default { 'Failed' } } Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Custom' -Status $FinalStatus -ResultMarkdown "Custom script execution failed: $($ErrorMessage.NormalizedError)" -Risk ($Script.Risk ?? 'Medium') -Name $ScriptName -Pillar $Script.Pillar -UserImpact $Script.UserImpact -ImplementationEffort $Script.ImplementationEffort -Category 'Custom Script' From f20c60a66bda10222492ab65cef58a258c4c7a2e Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Fri, 8 May 2026 10:44:19 +0800 Subject: [PATCH 15/36] Revert escaping --- Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 b/Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 index 221346f1bd04..deb66894da52 100644 --- a/Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 @@ -280,17 +280,9 @@ function New-CIPPAlertTemplate { } if ($Format -eq 'html') { - # Escape curly braces in content variables so the -f format operator - # does not interpret data values (e.g. JSON in drift/standards) as placeholders - $FmtTitle = [string]$Title -replace '\{', '{{' -replace '\}', '}}' - $FmtIntroText = [string]$IntroText -replace '\{', '{{' -replace '\}', '}}' - $FmtButtonUrl = [string]$ButtonUrl -replace '\{', '{{' -replace '\}', '}}' - $FmtButtonText = [string]$ButtonText -replace '\{', '{{' -replace '\}', '}}' - $FmtAfterButtonText = [string]$AfterButtonText -replace '\{', '{{' -replace '\}', '}}' - $FmtAuditLogLink = [string]$AuditLogLink -replace '\{', '{{' -replace '\}', '}}' - return [pscustomobject]@{ + return [pscustomobject]@{ title = $Title - htmlcontent = $HTMLTemplate -f $FmtTitle, $FmtIntroText, $FmtButtonUrl, $FmtButtonText, $FmtAfterButtonText, $FmtAuditLogLink + htmlcontent = $HTMLTemplate -f $Title, $IntroText, $ButtonUrl, $ButtonText, $AfterButtonText, $AuditLogLink } } elseif ($Format -eq 'json') { if ($InputObject -eq 'auditlog') { From 318d82612204d4ecac1a328b073ebdda24b55c83 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 7 May 2026 23:15:02 -0400 Subject: [PATCH 16/36] fix: correct assignment syntax for FieldValue in Add-CIPPBPAField function --- Modules/CIPPCore/Public/Add-CIPPBPAField.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Add-CIPPBPAField.ps1 b/Modules/CIPPCore/Public/Add-CIPPBPAField.ps1 index bed52e8cc786..74c1110550a6 100644 --- a/Modules/CIPPCore/Public/Add-CIPPBPAField.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPBPAField.ps1 @@ -34,7 +34,7 @@ function Add-CIPPBPAField { $Result[$fieldName] = [string]$JsonString } 'string' { - $Result[$fieldName], [string]$FieldValue + $Result[$fieldName] = [string]$FieldValue } } Add-CIPPAzDataTableEntity @Table -Entity $Result -Force From 51b22818735ef58a5f958be1e3a9e91715be93e0 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 8 May 2026 00:08:13 -0400 Subject: [PATCH 17/36] fix: add SharingCapability to current state retrieval in Invoke-CIPPStandardSPFileRequests function --- .../Invoke-CIPPStandardSPFileRequests.ps1 | 74 +++++++++++-------- 1 file changed, 44 insertions(+), 30 deletions(-) diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPFileRequests.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPFileRequests.ps1 index 5c3169485453..11cba32e784f 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPFileRequests.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPFileRequests.ps1 @@ -48,8 +48,15 @@ function Invoke-CIPPStandardSPFileRequests { return $true } + $SharingCapabilityEnum = @{ + 0L = 'Disabled' + 1L = 'External Users Only' + 2L = 'External Users and Guests (Anyone)' + 3L = 'Existing External Users Only' + } + try { - $CurrentState = Get-CIPPSPOTenant -TenantFilter $Tenant | Select-Object _ObjectIdentity_, TenantFilter, CoreRequestFilesLinkEnabled, OneDriveRequestFilesLinkEnabled, CoreRequestFilesLinkExpirationInDays, OneDriveRequestFilesLinkExpirationInDays + $CurrentState = Get-CIPPSPOTenant -TenantFilter $Tenant | Select-Object _ObjectIdentity_, TenantFilter, CoreRequestFilesLinkEnabled, OneDriveRequestFilesLinkEnabled, CoreRequestFilesLinkExpirationInDays, OneDriveRequestFilesLinkExpirationInDays, SharingCapability } catch { Write-LogMessage -API 'Standards' -tenant $tenant -message 'Failed to get current state of SPO tenant details' -sev Error return @@ -61,8 +68,8 @@ function Invoke-CIPPStandardSPFileRequests { return } - $WantedState = $Settings.state - $ExpirationDays = $Settings.expirationDays + $WantedState = [bool]$Settings.state + $ExpirationDays = if ($null -ne $Settings.expirationDays) { [int]$Settings.expirationDays } else { $null } $HumanReadableState = if ($WantedState -eq $true) { 'enabled' } else { 'disabled' } # Check if current state matches desired state @@ -83,34 +90,39 @@ function Invoke-CIPPStandardSPFileRequests { if ($Settings.remediate -eq $true) { if ($AllSettingsCorrect -eq $false) { - try { - $Properties = @{ - CoreRequestFilesLinkEnabled = $WantedState - OneDriveRequestFilesLinkEnabled = $WantedState - } - - # Add expiration settings if specified and feature is being enabled - if ($null -ne $ExpirationDays -and $WantedState -eq $true) { - $Properties['CoreRequestFilesLinkExpirationInDays'] = $ExpirationDays - $Properties['OneDriveRequestFilesLinkExpirationInDays'] = $ExpirationDays + if ($CurrentState.SharingCapability -ne 2) { + Write-LogMessage -API 'Standards' -tenant $tenant -message "Cannot set File Requests to $HumanReadableState because the Tenant sharing level is set to $($SharingCapabilityEnum[$CurrentState.SharingCapability]). The sharing level must be set to 'External Users and Guests (Anyone)' to remediate this standard. There may be a conflicting standard preventing this." -sev Error + } else { + try { + $Properties = @{ + CoreRequestFilesLinkEnabled = $WantedState + OneDriveRequestFilesLinkEnabled = $WantedState + } + + # Add expiration settings if specified and feature is being enabled + if ($null -ne $ExpirationDays -and $WantedState -eq $true) { + $Properties['CoreRequestFilesLinkExpirationInDays'] = $ExpirationDays + $Properties['OneDriveRequestFilesLinkExpirationInDays'] = $ExpirationDays + } + + $CurrentState | Set-CIPPSPOTenant -Properties $Properties + + # Reflect the just-applied state in-memory so the report block does not write + # the pre-remediation values into the drift compare field. + $CurrentState.CoreRequestFilesLinkEnabled = $WantedState + $CurrentState.OneDriveRequestFilesLinkEnabled = $WantedState + if ($null -ne $ExpirationDays -and $WantedState -eq $true) { + $CurrentState.CoreRequestFilesLinkExpirationInDays = $ExpirationDays + $CurrentState.OneDriveRequestFilesLinkExpirationInDays = $ExpirationDays + } + + $ExpirationMessage = if ($null -ne $ExpirationDays -and $WantedState -eq $true) { " with $ExpirationDays day expiration" } else { '' } + Write-LogMessage -API 'Standards' -tenant $tenant -message "Successfully set File Requests to $HumanReadableState$ExpirationMessage" -sev Info + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to set File Requests to $HumanReadableState. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage } - - $CurrentState | Set-CIPPSPOTenant -Properties $Properties - - # Reflect the just-applied state in-memory so the report block does not write - # the pre-remediation values into the drift compare field. - $CurrentState.CoreRequestFilesLinkEnabled = $WantedState - $CurrentState.OneDriveRequestFilesLinkEnabled = $WantedState - if ($null -ne $ExpirationDays -and $WantedState -eq $true) { - $CurrentState.CoreRequestFilesLinkExpirationInDays = $ExpirationDays - $CurrentState.OneDriveRequestFilesLinkExpirationInDays = $ExpirationDays - } - - $ExpirationMessage = if ($null -ne $ExpirationDays -and $WantedState -eq $true) { " with $ExpirationDays day expiration" } else { '' } - Write-LogMessage -API 'Standards' -tenant $tenant -message "Successfully set File Requests to $HumanReadableState$ExpirationMessage" -sev Info - } catch { - $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to set File Requests to $HumanReadableState. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage } } else { $ExpirationMessage = if ($null -ne $ExpirationDays -and $WantedState -eq $true) { " with $ExpirationDays day expiration" } else { '' } @@ -147,12 +159,14 @@ function Invoke-CIPPStandardSPFileRequests { OneDriveRequestFilesLinkEnabled = $CurrentState.OneDriveRequestFilesLinkEnabled CoreRequestFilesLinkExpirationInDays = $CurrentState.CoreRequestFilesLinkExpirationInDays OneDriveRequestFilesLinkExpirationInDays = $CurrentState.OneDriveRequestFilesLinkExpirationInDays + SharingCapability = $SharingCapabilityEnum[$CurrentState.SharingCapability] } $ExpectedValue = @{ CoreRequestFilesLinkEnabled = $WantedState OneDriveRequestFilesLinkEnabled = $WantedState CoreRequestFilesLinkExpirationInDays = if ($null -ne $ExpirationDays -and $WantedState -eq $true) { $ExpirationDays } else { $null } OneDriveRequestFilesLinkExpirationInDays = if ($null -ne $ExpirationDays -and $WantedState -eq $true) { $ExpirationDays } else { $null } + SharingCapability = 'External Users and Guests (Anyone)' } Set-CIPPStandardsCompareField -FieldName 'standards.SPFileRequests' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } From 7804aaf8d9bb24ceec54af7affdabf5702a3c660 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Fri, 8 May 2026 14:02:27 +0800 Subject: [PATCH 18/36] Try infer template type from content if missing, else fail early --- .../Public/Tools/Import-CommunityTemplate.ps1 | 19 ++++++++++++++-- .../Invoke-CIPPStandardIntuneTemplate.ps1 | 22 +++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Tools/Import-CommunityTemplate.ps1 b/Modules/CIPPCore/Public/Tools/Import-CommunityTemplate.ps1 index 603df66a4c96..56d3c5843e44 100644 --- a/Modules/CIPPCore/Public/Tools/Import-CommunityTemplate.ps1 +++ b/Modules/CIPPCore/Public/Tools/Import-CommunityTemplate.ps1 @@ -68,7 +68,7 @@ function Import-CommunityTemplate { $Template | Add-Member -MemberType NoteProperty -Name SHA -Value $SHA -Force $Template | Add-Member -MemberType NoteProperty -Name Source -Value $Source -Force Add-CIPPAzDataTableEntity @Table -Entity $Template -Force - + if ($Existing -and $Existing.SHA -ne $SHA) { $StatusMessage = "Updated template '$($Template.RowKey)' from source '$Source' (SHA changed)." } elseif ($Existing) { @@ -217,6 +217,21 @@ function Import-CommunityTemplate { '*managedAppPolicies*' { 'AppProtection' } '*deviceAppManagement*' { 'AppProtection' } } + + # Fallback: infer type from template content when @odata.id is missing or unrecognized + if (-not $URLName) { + $odataType = $Template.'@odata.type' + $URLName = if ($null -ne $Template.settings -and $null -ne $Template.technologies) { 'Catalog' } + elseif ($null -ne $Template.scheduledActionsForRule -or $odataType -match 'CompliancePolicy') { 'DeviceCompliancePolicies' } + elseif ($odataType -match 'windowsDriverUpdateProfile') { 'windowsDriverUpdateProfiles' } + elseif ($odataType -match 'ManagedApp|managedAppProtection') { 'AppProtection' } + elseif ($odataType -match 'deviceConfiguration|#microsoft\.graph\.\w+Configuration$') { 'Device' } + else { $null } + if ($URLName) { + Write-Information "Inferred Intune template type '$URLName' from content structure for '$($Template.displayName ?? $Template.Name)'" + } + } + $RawJson = $Template | Select-Object * -ExcludeProperty id, lastModifiedDateTime, 'assignments', '#microsoft*', '*@odata.navigationLink', '*@odata.associationLink', '*@odata.context', 'ScopeTagIds', 'supportsScopeTags', 'createdDateTime', '@odata.id', '@odata.editLink', 'lastModifiedDateTime@odata.type', 'roleScopeTagIds@odata.type', createdDateTime, 'createdDateTime@odata.type' Remove-ODataProperties -Object $RawJson $RawJson = $RawJson | ConvertTo-Json -Depth 100 -Compress @@ -288,6 +303,6 @@ function Import-CommunityTemplate { Write-Warning $StatusMessage Write-Information $_.InvocationInfo.PositionMessage } - + return $StatusMessage } diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1 index bb1cf5dd4ca6..7af9715f79db 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1 @@ -75,6 +75,28 @@ function Invoke-CIPPStandardIntuneTemplate { $RawJSON = $rawJsonFromTemplate $TemplateType = $Template.Type + # Fallback: infer type from RAWJson content when stored template has no Type + if (-not $TemplateType) { + try { + $parsedRaw = $rawJsonFromTemplate | ConvertFrom-Json -ErrorAction SilentlyContinue + $odataType = $parsedRaw.'@odata.type' + $TemplateType = if ($null -ne $parsedRaw.settings -and $null -ne $parsedRaw.technologies) { 'Catalog' } + elseif ($null -ne $parsedRaw.scheduledActionsForRule -or $odataType -match 'CompliancePolicy') { 'deviceCompliancePolicies' } + elseif ($odataType -match 'windowsDriverUpdateProfile') { 'windowsDriverUpdateProfiles' } + elseif ($odataType -match 'ManagedApp|managedAppProtection') { 'AppProtection' } + elseif ($odataType -match 'deviceConfiguration|#microsoft\.graph\.\w+Configuration$') { 'Device' } + else { $null } + } catch { + $TemplateType = $null + } + if ($TemplateType) { + Write-Information "[IntuneTemplate][$Tenant] Inferred template type '$TemplateType' from content for '$displayname'" + } else { + Write-LogMessage -API 'Standards' -tenant $tenant -message "Intune Template '$displayname' has no Type and type could not be inferred. Re-import the template to fix." -sev 'Error' + return $true + } + } + $AssignmentsMatch = $null try { $ExistingPolicy = Get-CIPPIntunePolicy -tenantFilter $Tenant -DisplayName $displayname -TemplateType $TemplateType From 8d454b9566497fb2577c76d50c8ee4a75562e5e1 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 8 May 2026 12:01:35 +0200 Subject: [PATCH 19/36] fixed unmapped issue sherweb --- .../HTTP Functions/Invoke-ListCSPsku.ps1 | 13 +++++++++---- .../Sherweb/Get-SherwebCurrentSubscription.ps1 | 4 ++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListCSPsku.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListCSPsku.ps1 index 094f651314e2..b317057f1870 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListCSPsku.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListCSPsku.ps1 @@ -19,11 +19,16 @@ function Invoke-ListCSPsku { } $StatusCode = [HttpStatusCode]::OK } catch { - $GraphRequest = [PSCustomObject]@{ - name = @(@{value = 'Error getting catalog' }) - sku = $_.Exception.Message + if ($_.Exception.Message -eq 'No Sherweb mapping found') { + $GraphRequest = @() + $StatusCode = [HttpStatusCode]::OK + } else { + $GraphRequest = [PSCustomObject]@{ + name = @(@{value = 'Error getting catalog' }) + sku = $_.Exception.Message + } + $StatusCode = [HttpStatusCode]::InternalServerError } - $StatusCode = [HttpStatusCode]::InternalServerError } return [HttpResponseContext]@{ diff --git a/Modules/CippExtensions/Public/Sherweb/Get-SherwebCurrentSubscription.ps1 b/Modules/CippExtensions/Public/Sherweb/Get-SherwebCurrentSubscription.ps1 index f0cf0308dc7c..bfdbf623ca80 100644 --- a/Modules/CippExtensions/Public/Sherweb/Get-SherwebCurrentSubscription.ps1 +++ b/Modules/CippExtensions/Public/Sherweb/Get-SherwebCurrentSubscription.ps1 @@ -11,6 +11,10 @@ function Get-SherwebCurrentSubscription { $CustomerId = Get-ExtensionMapping -Extension 'Sherweb' | Where-Object { $_.RowKey -eq $TenantFilter } | Select-Object -ExpandProperty IntegrationId } + if ([string]::IsNullOrEmpty($CustomerId)) { + throw 'No Sherweb mapping found' + } + Write-Information "Getting current subscription for $CustomerId" $AuthHeader = Get-SherwebAuthentication $Uri = "https://api.sherweb.com/service-provider/v1/billing/subscriptions/details?customerId=$CustomerId" From 85d9a6c228dee577ba5cded5bab28255ce069d9a Mon Sep 17 00:00:00 2001 From: Roel van der Wegen Date: Fri, 8 May 2026 12:07:51 +0200 Subject: [PATCH 20/36] concept --- .../Invoke-ListObjectHistory.ps1 | 284 ++++++++++++++++++ 1 file changed, 284 insertions(+) create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListObjectHistory.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListObjectHistory.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListObjectHistory.ps1 new file mode 100644 index 000000000000..68ed78012e63 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListObjectHistory.ps1 @@ -0,0 +1,284 @@ +function Invoke-ListObjectHistory { + <# + .SYNOPSIS + In progress concept - Rvd + Returns a transformed timeline of audit events for any tenant object over a configurable period. + + .DESCRIPTION + Aggregates change history from Graph directoryAudits and (for Exchange objects) the Unified Audit Log. + Returns a normalised timeline array with parsed property changes, actor details, and source metadata. + Supports users, groups, applications, service principals, devices, administrative units, + conditional access policies, shared mailboxes, distribution lists, and mail contacts. + + .FUNCTIONALITY + Entrypoint + .ROLE + Identity.AuditLog.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter + $ObjectId = $Request.Query.objectId ?? $Request.Query.id ?? $Request.Body.objectId ?? $Request.Body.id + $ObjectType = $Request.Query.objectType ?? $Request.Body.objectType + $Days = try { [int]($Request.Query.days ?? $Request.Body.days ?? 30) } catch { 30 } + + #region Validation + if (-not $ObjectId) { + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = @{ Results = 'Error: objectId is required' } + } + } + + try { + $ObjectId = ConvertTo-CIPPODataFilterValue -Value $ObjectId -Type Guid + } catch { + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = @{ Results = 'Error: objectId must be a valid GUID' } + } + } + + if (-not $TenantFilter) { + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = @{ Results = 'Error: tenantFilter is required' } + } + } + + if ($Days -lt 1) { $Days = 1 } + if ($Days -gt 90) { $Days = 90 } + #endregion + + $StartTime = (Get-Date).AddDays(-$Days).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') + $TypeKey = ($ObjectType ?? '').Trim().ToLowerInvariant() + $Warnings = [System.Collections.Generic.List[string]]::new() + $Sources = [System.Collections.Generic.List[string]]::new() + + #region Resolve object and classify source lanes + $ResolveUri = if ($TypeKey -eq 'conditionalaccesspolicy') { + "https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies/$ObjectId" + } else { + "https://graph.microsoft.com/v1.0/directoryObjects/$ObjectId" + } + + $ResolvedObject = $null + $ResolvedAs = $null + $ResolvedDisplayName = $null + try { + $ResolvedObject = New-GraphGetRequest -uri $ResolveUri -tenantid $TenantFilter -ErrorAction Stop + $ODataType = $ResolvedObject.'@odata.type' -replace '^#microsoft\.graph\.', '' + $ResolvedAs = if ($TypeKey -eq 'conditionalaccesspolicy') { 'conditionalAccessPolicy' } else { $ODataType ?? 'directoryObject' } + $ResolvedDisplayName = $ResolvedObject.displayName ?? $ResolvedObject.userPrincipalName ?? $ResolvedObject.appId ?? $ObjectId + } catch { + $ResolvedAs = if ($TypeKey) { $TypeKey } else { 'directoryObject' } + $ResolvedDisplayName = $ObjectId + } + + # Classify which audit sources to query based on resolved type + $ExchangeOnlyTypes = @('mailbox', 'sharedmailbox', 'distributionlist', 'mailcontact', 'resource', 'roommailbox', 'equipmentmailbox') + $EntraOnlyTypes = @('application', 'serviceprincipal', 'device', 'administrativeunit', 'conditionalaccesspolicy') + + $QueryDirectoryAudits = $true + $QueryExchangeAudit = $false + $ExchangeAnchor = $null + + if ($TypeKey -in $ExchangeOnlyTypes) { + # Caller explicitly said this is an Exchange object — skip Graph, go Exchange only + $QueryDirectoryAudits = $false + $QueryExchangeAudit = $true + $ExchangeAnchor = $ResolvedObject.userPrincipalName ?? $ResolvedObject.mail + } elseif ($TypeKey -in $EntraOnlyTypes) { + # Pure Entra object — Graph directoryAudits only + $QueryDirectoryAudits = $true + $QueryExchangeAudit = $false + } elseif ($ResolvedObject.mail -and $ResolvedAs -in @('user', 'group')) { + # Mail-enabled user or group — query both + $QueryDirectoryAudits = $true + $QueryExchangeAudit = $true + $ExchangeAnchor = $ResolvedObject.userPrincipalName ?? $ResolvedObject.mail + } + # else: unknown type or resolution failed — default is Graph directoryAudits only + #endregion + + #region Helper: parse modifiedProperties + $ParseModifiedProperties = { + param([array]$Properties) + foreach ($Prop in $Properties) { + $OldVal = $null + $NewVal = $null + if ($Prop.oldValue -and $Prop.oldValue -ne '[]' -and $Prop.oldValue -ne 'null') { + $OldVal = try { $Prop.oldValue | ConvertFrom-Json -ErrorAction Stop } catch { $Prop.oldValue } + } + if ($Prop.newValue -and $Prop.newValue -ne '[]' -and $Prop.newValue -ne 'null') { + $NewVal = try { $Prop.newValue | ConvertFrom-Json -ErrorAction Stop } catch { $Prop.newValue } + } + if ($null -ne $OldVal -or $null -ne $NewVal) { + [PSCustomObject]@{ + property = $Prop.displayName + oldValue = $OldVal + newValue = $NewVal + } + } + } + } + #endregion + + #region Helper: normalize initiatedBy + $NormalizeActor = { + param($InitiatedBy) + if ($InitiatedBy.user) { + [PSCustomObject]@{ + displayName = $InitiatedBy.user.displayName ?? $InitiatedBy.user.userPrincipalName + id = $InitiatedBy.user.id + upn = $InitiatedBy.user.userPrincipalName + type = 'user' + } + } elseif ($InitiatedBy.app) { + [PSCustomObject]@{ + displayName = $InitiatedBy.app.displayName + id = $InitiatedBy.app.servicePrincipalId ?? $InitiatedBy.app.appId + upn = $null + type = 'app' + } + } else { + [PSCustomObject]@{ + displayName = 'Unknown' + id = $null + upn = $null + type = 'unknown' + } + } + } + #endregion + + #region Query Graph directoryAudits + [array]$DirectoryTimeline = @() + if ($QueryDirectoryAudits) { + try { + $Filter = "activityDateTime ge $StartTime and targetResources/any(s:s/id eq '$ObjectId')" + $Uri = "https://graph.microsoft.com/v1.0/auditLogs/directoryAudits?`$filter=$Filter&`$orderby=activityDateTime desc" + + Write-LogMessage -API $APIName -message "Object history: querying directoryAudits for $ObjectId (last $Days days)" -Sev 'Debug' -tenant $TenantFilter + + [array]$RawAudits = New-GraphGetRequest -uri $Uri -tenantid $TenantFilter -ComplexFilter -ErrorAction Stop + + [array]$DirectoryTimeline = @(foreach ($Event in $RawAudits) { + $TargetResource = $Event.targetResources | Where-Object { $_.id -eq $ObjectId } | Select-Object -First 1 + $TargetResource = $TargetResource ?? ($Event.targetResources | Select-Object -First 1) + + [PSCustomObject]@{ + id = $Event.id + timestamp = $Event.activityDateTime + activity = $Event.activityDisplayName + category = $Event.category + operationType = $Event.operationType + result = $Event.result + actor = & $NormalizeActor $Event.initiatedBy + target = $TargetResource.displayName ?? $TargetResource.userPrincipalName ?? $ObjectId + changes = @(& $ParseModifiedProperties ($TargetResource.modifiedProperties ?? @())) + source = 'directoryAudit' + } + }) + [void]$Sources.Add('directoryAudit') + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API $APIName -tenant $TenantFilter -message "Object history: directoryAudits failed - $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + [void]$Warnings.Add("Directory audit query failed: $($ErrorMessage.NormalizedError)") + } + } + #endregion + + #region Query Exchange Unified Audit Log + [array]$ExchangeTimeline = @() + if ($QueryExchangeAudit) { + if ($ExchangeAnchor) { + try { + $SessionId = "ObjectHistory_$(Get-Random -Minimum 10000 -Maximum 99999)" + $SearchParam = @{ + SessionCommand = 'ReturnLargeSet' + ObjectIds = @($ExchangeAnchor) + SessionId = $SessionId + StartDate = (Get-Date).AddDays(-$Days) + EndDate = (Get-Date) + ResultSize = 5000 + } + + $ExchangeLogs = [System.Collections.Generic.List[object]]::new() + $MaxPages = 10 + $Page = 0 + do { + $Batch = @(New-ExoRequest -tenantid $TenantFilter -cmdlet 'Search-UnifiedAuditLog' -cmdParams $SearchParam -Anchor $ExchangeAnchor) + foreach ($Item in $Batch) { [void]$ExchangeLogs.Add($Item) } + $Page++ + } while ($Batch.Count -eq 5000 -and $Page -lt $MaxPages) + + [array]$ExchangeTimeline = @(foreach ($Log in $ExchangeLogs) { + $AuditData = try { $Log.AuditData | ConvertFrom-Json -ErrorAction Stop } catch { $null } + if (-not $AuditData) { continue } + + [PSCustomObject]@{ + id = $AuditData.Id ?? $Log.Identity + timestamp = $Log.CreationDate ?? $AuditData.CreationTime + activity = $AuditData.Operation + category = 'ExchangeItem' + operationType = $AuditData.Operation + result = if ($AuditData.ResultStatus -eq 'Succeeded' -or $AuditData.ResultStatus -eq 'True') { 'success' } else { $AuditData.ResultStatus ?? 'success' } + actor = [PSCustomObject]@{ + displayName = $AuditData.UserId + id = $AuditData.UserId + upn = $AuditData.UserId + type = 'user' + } + target = $AuditData.ObjectId ?? $ExchangeAnchor + changes = @( + if ($AuditData.Parameters) { + foreach ($Param in $AuditData.Parameters) { + [PSCustomObject]@{ + property = $Param.Name + oldValue = $null + newValue = $Param.Value + } + } + } + ) + source = 'exchangeAudit' + } + }) + [void]$Sources.Add('exchangeAudit') + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API $APIName -tenant $TenantFilter -message "Object history: Exchange UAL failed - $($ErrorMessage.NormalizedError)" -sev Warning -LogData $ErrorMessage + [void]$Warnings.Add("Exchange audit query failed: $($ErrorMessage.NormalizedError)") + } + } else { + [void]$Warnings.Add('Exchange audit skipped: could not determine mailbox anchor (UPN/mail)') + } + } + #endregion + + #region Merge and sort timeline + $Timeline = @($DirectoryTimeline + $ExchangeTimeline | Where-Object { $_ } | Sort-Object -Property timestamp -Descending) + #endregion + + $Body = [PSCustomObject]@{ + objectId = $ObjectId + objectType = $ObjectType + resolvedObject = $ResolvedObject + resolvedAs = $ResolvedAs + resolvedDisplayName = $ResolvedDisplayName + days = $Days + activityFromUtc = $StartTime + totalEvents = $Timeline.Count + sources = @($Sources) + warnings = @($Warnings) + timeline = @($Timeline) + } + + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = $Body + } +} From 5644578fe74abd3f331fbc0cd4adfac1becfab2c Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 8 May 2026 12:18:10 +0200 Subject: [PATCH 21/36] fixed #5930 --- .../Invoke-ExecQuarantineManagement.ps1 | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ExecQuarantineManagement.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ExecQuarantineManagement.ps1 index deb23870dd96..44f942384722 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ExecQuarantineManagement.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ExecQuarantineManagement.ps1 @@ -18,14 +18,19 @@ function Invoke-ExecQuarantineManagement { if ($ActionType -eq 'Release') { $params['ReleaseToAll'] = $true + if ($Request.Body.Identity -is [string]) { + $params['Identity'] = $Request.Body.Identity + } else { + $params['Identities'] = $Request.Body.Identity + $params['Identity'] = '000' + } } else { $params['ActionType'] = $ActionType - } - - if ($Request.Body.Identity -is [string]) { - $params['Identity'] = $Request.Body.Identity - } else { - $params['Identities'] = $Request.Body.Identity + if ($Request.Body.Identity -is [string]) { + $params['Identities'] = @($Request.Body.Identity) + } else { + $params['Identities'] = $Request.Body.Identity + } $params['Identity'] = '000' } New-ExoRequest -tenantid $TenantFilter -cmdlet 'Release-QuarantineMessage' -cmdParams $params From 67ea958492a04d806604db545165157941b59063 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 8 May 2026 12:24:48 +0200 Subject: [PATCH 22/36] fixes #5973 --- .../Public/Standards/Invoke-CIPPStandardSpoofWarn.ps1 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSpoofWarn.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSpoofWarn.ps1 index 6fa95268d1cc..3ef10d9e5e15 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSpoofWarn.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSpoofWarn.ps1 @@ -58,6 +58,9 @@ function Invoke-CIPPStandardSpoofWarn { return } + # Sanitize AllowList — the API may return @('') instead of @() for an empty list + $CurrentInfo.AllowList = @($CurrentInfo.AllowList | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) + # Get state value using null-coalescing operator $state = $Settings.state.value ?? $Settings.state From b7773c632ef1e955e57f6ced95b56eaba01403eb Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 8 May 2026 12:33:44 +0200 Subject: [PATCH 23/36] pushing new compliance menus --- .../Invoke-AddRetentionCompliancePolicy.ps1 | 47 ++++++++++++++++++ ...e-AddRetentionCompliancePolicyTemplate.ps1 | 48 +++++++++++++++++++ .../Invoke-EditRetentionCompliancePolicy.ps1 | 46 ++++++++++++++++++ .../Invoke-ListRetentionCompliancePolicy.ps1 | 30 ++++++++++++ ...ListRetentionCompliancePolicyTemplates.ps1 | 26 ++++++++++ ...Invoke-RemoveRetentionCompliancePolicy.ps1 | 39 +++++++++++++++ ...emoveRetentionCompliancePolicyTemplate.ps1 | 36 ++++++++++++++ 7 files changed, 272 insertions(+) create mode 100644 Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-AddRetentionCompliancePolicy.ps1 create mode 100644 Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-AddRetentionCompliancePolicyTemplate.ps1 create mode 100644 Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-EditRetentionCompliancePolicy.ps1 create mode 100644 Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-ListRetentionCompliancePolicy.ps1 create mode 100644 Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-ListRetentionCompliancePolicyTemplates.ps1 create mode 100644 Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-RemoveRetentionCompliancePolicy.ps1 create mode 100644 Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-RemoveRetentionCompliancePolicyTemplate.ps1 diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-AddRetentionCompliancePolicy.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-AddRetentionCompliancePolicy.ps1 new file mode 100644 index 000000000000..fec42ad1649a --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-AddRetentionCompliancePolicy.ps1 @@ -0,0 +1,47 @@ +Function Invoke-AddRetentionCompliancePolicy { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.RetentionCompliancePolicy.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + $RequestParams = $Request.Body.PowerShellCommand | ConvertFrom-Json | Select-Object -Property * -ExcludeProperty GUID, comments, RuleParams + $RuleParams = ($Request.Body.PowerShellCommand | ConvertFrom-Json).RuleParams + + $Tenants = ($Request.Body.selectedTenants).value + $Result = foreach ($TenantFilter in $Tenants) { + try { + $PolicyParams = $RequestParams | Select-Object -Property * -ExcludeProperty RuleParams + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'New-RetentionCompliancePolicy' -cmdParams $PolicyParams -Compliance -AsApp -useSystemMailbox $true + + if ($RuleParams) { + $RuleHash = @{} + $RuleParams.PSObject.Properties | ForEach-Object { $RuleHash[$_.Name] = $_.Value } + $RuleHash['Policy'] = $RequestParams.Name + if (-not $RuleHash.ContainsKey('Name') -or [string]::IsNullOrWhiteSpace($RuleHash['Name'])) { + $RuleHash['Name'] = "$($RequestParams.Name) Rule" + } + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'New-RetentionComplianceRule' -cmdParams $RuleHash -Compliance -AsApp -useSystemMailbox $true + } + + "Successfully created Retention compliance policy $($RequestParams.Name) for $TenantFilter." + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Successfully created Retention compliance policy $($RequestParams.Name) for $TenantFilter." -sev Info + } catch { + $ErrorMessage = Get-CippException -Exception $_ + "Could not create Retention compliance policy for $($TenantFilter): $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Could not create Retention compliance policy for $($TenantFilter): $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + } + } + + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = @{Results = @($Result) } + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-AddRetentionCompliancePolicyTemplate.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-AddRetentionCompliancePolicyTemplate.ps1 new file mode 100644 index 000000000000..79896013ae2d --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-AddRetentionCompliancePolicyTemplate.ps1 @@ -0,0 +1,48 @@ +Function Invoke-AddRetentionCompliancePolicyTemplate { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.RetentionCompliancePolicy.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + try { + $GUID = (New-Guid).GUID + $JSON = if ($Request.Body.PowerShellCommand) { + $Request.Body.PowerShellCommand | ConvertFrom-Json + } else { + ([pscustomobject]$Request.Body | Select-Object Name, Comment, Enabled, RestrictiveRetention, ExchangeLocation, ExchangeLocationException, ModernGroupLocation, ModernGroupLocationException, OneDriveLocation, OneDriveLocationException, SharePointLocation, SharePointLocationException, SkypeLocation, SkypeLocationException, PublicFolderLocation, TeamsChannelLocation, TeamsChannelLocationException, TeamsChatLocation, TeamsChatLocationException, ApplyComplianceTag, RetentionDuration, RetentionAction, RetentionDurationDisplayHint, ExpirationDateOption, RuleParams) | ForEach-Object { + $NonEmptyProperties = $_.PSObject.Properties | Where-Object { $null -ne $_.Value } | Select-Object -ExpandProperty Name + $_ | Select-Object -Property $NonEmptyProperties + } + } + + $JSON = ($JSON | Select-Object @{n = 'name'; e = { $_.Name ?? $_.name } }, @{n = 'comments'; e = { $_.Comment ?? $_.comments } }, * | ConvertTo-Json -Depth 10) + $Table = Get-CippTable -tablename 'templates' + $Table.Force = $true + Add-CIPPAzDataTableEntity @Table -Entity @{ + JSON = "$JSON" + RowKey = "$GUID" + PartitionKey = 'RetentionCompliancePolicyTemplate' + } + $Result = "Successfully created Retention Compliance Policy Template: $($Request.Body.Name ?? $Request.Body.name) with GUID $GUID" + Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Debug' + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to create Retention Compliance Policy Template: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Error' -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::InternalServerError + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{Results = $Result } + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-EditRetentionCompliancePolicy.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-EditRetentionCompliancePolicy.ps1 new file mode 100644 index 000000000000..72db204c7d83 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-EditRetentionCompliancePolicy.ps1 @@ -0,0 +1,46 @@ +Function Invoke-EditRetentionCompliancePolicy { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.RetentionCompliancePolicy.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter + $Identity = $Request.Query.Identity ?? $Request.Body.Identity ?? $Request.Body.Name + $State = $Request.Query.State ?? $Request.Body.State + + try { + $Params = @{ + Identity = $Identity + } + + if ($State) { + $Params['Enabled'] = ($State -eq 'enable' -or $State -eq $true -or $State -eq 'true') + } + + if ($Request.Body.parameters) { + $Request.Body.parameters.PSObject.Properties | ForEach-Object { + if ($_.Name -ne 'Identity') { $Params[$_.Name] = $_.Value } + } + } + + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Set-RetentionCompliancePolicy' -cmdParams $Params -Compliance -AsApp -useSystemMailbox $true + $Result = "Updated Retention compliance policy $Identity" + Write-LogMessage -Headers $Request.Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Info + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed updating Retention compliance policy $Identity. Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -Headers $Request.Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Error -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::Forbidden + } + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{Results = $Result } + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-ListRetentionCompliancePolicy.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-ListRetentionCompliancePolicy.ps1 new file mode 100644 index 000000000000..1c5290932c8f --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-ListRetentionCompliancePolicy.ps1 @@ -0,0 +1,30 @@ +Function Invoke-ListRetentionCompliancePolicy { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.RetentionCompliancePolicy.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + $TenantFilter = $Request.Query.tenantFilter + + try { + $Policies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-RetentionCompliancePolicy' -Compliance -AsApp | Select-Object * -ExcludeProperty *odata*, *data.type* + $Rules = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-RetentionComplianceRule' -Compliance -AsApp | Select-Object * -ExcludeProperty *odata*, *data.type* + $GraphRequest = $Policies | Select-Object *, + @{l = 'AssociatedRules'; e = { $name = $_.Name; @($Rules | Where-Object { $_.Policy -eq $name }) } }, + @{l = 'RuleCount'; e = { $name = $_.Name; (@($Rules | Where-Object { $_.Policy -eq $name })).Count } } + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + $StatusCode = [HttpStatusCode]::Forbidden + $GraphRequest = $ErrorMessage + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @($GraphRequest) + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-ListRetentionCompliancePolicyTemplates.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-ListRetentionCompliancePolicyTemplates.ps1 new file mode 100644 index 000000000000..98dace219fbc --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-ListRetentionCompliancePolicyTemplates.ps1 @@ -0,0 +1,26 @@ +Function Invoke-ListRetentionCompliancePolicyTemplates { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + Security.RetentionCompliancePolicy.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + $Table = Get-CippTable -tablename 'templates' + $Filter = "PartitionKey eq 'RetentionCompliancePolicyTemplate'" + $Templates = (Get-CIPPAzDataTableEntity @Table -Filter $Filter) | ForEach-Object { + $GUID = $_.RowKey + $data = $_.JSON | ConvertFrom-Json + $data | Add-Member -NotePropertyName 'GUID' -NotePropertyValue $GUID -Force + $data + } + + if ($Request.Query.ID) { $Templates = $Templates | Where-Object -Property GUID -EQ $Request.Query.ID } + + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = @($Templates) + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-RemoveRetentionCompliancePolicy.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-RemoveRetentionCompliancePolicy.ps1 new file mode 100644 index 000000000000..a6061dc75730 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-RemoveRetentionCompliancePolicy.ps1 @@ -0,0 +1,39 @@ +Function Invoke-RemoveRetentionCompliancePolicy { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.RetentionCompliancePolicy.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter + $Identity = $Request.Query.Identity ?? $Request.Body.Identity ?? $Request.Body.Name + $ForceRemoval = $Request.Body.ForceDeletion -eq $true + + try { + $Params = @{ + Identity = $Identity + } + if ($ForceRemoval) { $Params['ForceDeletion'] = $true } + + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Remove-RetentionCompliancePolicy' -cmdParams $Params -Compliance -AsApp -useSystemMailbox $true + $Result = "Deleted Retention compliance policy $Identity" + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Info + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to delete Retention compliance policy $Identity - $($ErrorMessage.NormalizedError)" + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Error -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::Forbidden + } + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{Results = $Result } + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-RemoveRetentionCompliancePolicyTemplate.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-RemoveRetentionCompliancePolicyTemplate.ps1 new file mode 100644 index 000000000000..69d7b60eca4e --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-RemoveRetentionCompliancePolicyTemplate.ps1 @@ -0,0 +1,36 @@ +Function Invoke-RemoveRetentionCompliancePolicyTemplate { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.RetentionCompliancePolicy.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + $ID = $Request.Body.ID ?? $Request.Query.ID + try { + $Table = Get-CippTable -tablename 'templates' + $SafeID = ConvertTo-CIPPODataFilterValue -Value $ID -Type Guid + $Filter = "PartitionKey eq 'RetentionCompliancePolicyTemplate' and RowKey eq '$SafeID'" + $ClearRow = Get-CIPPAzDataTableEntity @Table -Filter $Filter -Property PartitionKey, RowKey + Remove-AzDataTableEntity -Force @Table -Entity $ClearRow + $Result = "Removed Retention Compliance Policy template with ID $ID" + Write-LogMessage -Headers $Headers -API $APIName -message $Result -Sev 'Info' + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to remove Retention Compliance Policy template $ID. $($ErrorMessage.NormalizedError)" + Write-LogMessage -Headers $Headers -API $APIName -message $Result -Sev 'Error' -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::Forbidden + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{'Results' = $Result } + }) + +} From 584a6fe6479840c71cca4a8b5205e735b76fe919 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Fri, 8 May 2026 12:37:09 +0200 Subject: [PATCH 24/36] feat: support bulk manager and sponsor updates - Updated Set-CIPPManager and Set-CIPPSponsor functions to accept multiple users for bulk processing. - Modified Invoke-EditUser and Invoke-PatchUser to handle bulk updates for managers and sponsors. - Enhanced logging for success and error messages during updates. --- Modules/CIPPCore/Public/New-CIPPUserTask.ps1 | 8 +- Modules/CIPPCore/Public/Set-CIPPManager.ps1 | 52 +++++-- Modules/CIPPCore/Public/Set-CIPPSponsor.ps1 | 51 +++++-- .../Administration/Users/Invoke-EditUser.ps1 | 8 +- .../Administration/Users/Invoke-PatchUser.ps1 | 140 ++++++++++++++---- 5 files changed, 198 insertions(+), 61 deletions(-) diff --git a/Modules/CIPPCore/Public/New-CIPPUserTask.ps1 b/Modules/CIPPCore/Public/New-CIPPUserTask.ps1 index a91904f4724a..7fdfab465719 100644 --- a/Modules/CIPPCore/Public/New-CIPPUserTask.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPUserTask.ps1 @@ -81,13 +81,13 @@ function New-CIPPUserTask { } if ($UserObj.setManager) { - $ManagerResult = Set-CIPPManager -User $CreationResults.Username -Manager $UserObj.setManager.value -TenantFilter $UserObj.tenantFilter -Headers $Headers - $Results.Add($ManagerResult) + $ManagerResults = Set-CIPPManager -Users $CreationResults.Username -Manager $UserObj.setManager.value -TenantFilter $UserObj.tenantFilter -Headers $Headers + $Results.Add($ManagerResults.Result) } if ($UserObj.setSponsor) { - $SponsorResult = Set-CIPPSponsor -User $CreationResults.Username -Sponsor $UserObj.setSponsor.value -TenantFilter $UserObj.tenantFilter -Headers $Headers - $Results.Add($SponsorResult) + $SponsorResults = Set-CIPPSponsor -Users $CreationResults.Username -Sponsor $UserObj.setSponsor.value -TenantFilter $UserObj.tenantFilter -Headers $Headers + $Results.Add($SponsorResults.Result) } return @{ diff --git a/Modules/CIPPCore/Public/Set-CIPPManager.ps1 b/Modules/CIPPCore/Public/Set-CIPPManager.ps1 index c1394b04936d..b71367bceb88 100644 --- a/Modules/CIPPCore/Public/Set-CIPPManager.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPManager.ps1 @@ -1,23 +1,49 @@ function Set-CIPPManager { [CmdletBinding()] param ( - $User, - $Manager, + [Alias('User')] + [string[]] $Users, + [string] $Manager, $TenantFilter, $APIName = 'Set Manager', $Headers ) - try { - $ManagerBody = [PSCustomObject]@{'@odata.id' = "https://graph.microsoft.com/beta/users/$($Manager)" } - $ManagerBodyJSON = ConvertTo-Json -Compress -Depth 10 -InputObject $ManagerBody - $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/users/$($User)/manager/`$ref" -tenantid $TenantFilter -type PUT -body $ManagerBodyJSON - Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Set $User's manager to $Manager" -Sev 'Info' - } catch { - $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to Set Manager. Error:$($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $_ - throw "Failed to set manager: $($ErrorMessage.NormalizedError)" + if ($Users.Count -eq 0) { + return @() } - return "Set $User's manager to $Manager" -} + $RequestId = 0 + $Requests = foreach ($User in $Users) { + @{ + id = ($RequestId++).ToString() + method = 'PUT' + url = "users/$User/manager/`$ref" + body = @{ '@odata.id' = "https://graph.microsoft.com/beta/users/$Manager" } + headers = @{ 'Content-Type' = 'application/json' } + } + } + + $Responses = New-GraphBulkRequest -tenantid $TenantFilter -Requests @($Requests) + + $Results = foreach ($Response in @($Responses)) { + $ResponseIndex = [int]$Response.id + $User = $Users[$ResponseIndex] + $Success = [int]$Response.status -in @(200, 204) + $ErrorMessage = if ($Response.body.error.message) { $Response.body.error.message } else { "Unknown error (Status: $($Response.status))" } + $Result = if ($Success) { "Set $User's manager to $Manager" } else { "Failed to set $User's manager: $ErrorMessage" } + $Severity = if ($Success) { 'Info' } else { 'Error' } + + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev $Severity + + [pscustomobject]@{ + User = $User + Manager = $Manager + Success = $Success + Result = $Result + Status = $Response.status + } + } + + return @($Results) +} diff --git a/Modules/CIPPCore/Public/Set-CIPPSponsor.ps1 b/Modules/CIPPCore/Public/Set-CIPPSponsor.ps1 index ad2444c01b9f..8bbb7bfb9850 100644 --- a/Modules/CIPPCore/Public/Set-CIPPSponsor.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPSponsor.ps1 @@ -1,22 +1,49 @@ function Set-CIPPSponsor { [CmdletBinding()] param ( - $User, - $Sponsor, + [Alias('User')] + [string[]] $Users, + [string] $Sponsor, $TenantFilter, $APIName = 'Set Sponsor', $Headers ) - try { - $SponsorBody = [PSCustomObject]@{'@odata.id' = "https://graph.microsoft.com/beta/users/$($Sponsor)" } - $SponsorBodyJSON = ConvertTo-Json -Compress -Depth 10 -InputObject $SponsorBody - $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/users/$($User)/sponsors/`$ref" -tenantid $TenantFilter -type PUT -body $SponsorBodyJSON - Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Set $User's sponsor to $Sponsor" -Sev 'Info' - } catch { - $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to Set Sponsor. Error:$($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $_ - throw "Failed to set sponsor: $($_.Exception.Message)" + if ($Users.Count -eq 0) { + return @() } - return "Set $user's sponsor to $Sponsor" + + $RequestId = 0 + $Requests = foreach ($User in $Users) { + @{ + id = ($RequestId++).ToString() + method = 'PUT' + url = "users/$User/sponsors/`$ref" + body = @{ '@odata.id' = "https://graph.microsoft.com/beta/users/$Sponsor" } + headers = @{ 'Content-Type' = 'application/json' } + } + } + + $Responses = New-GraphBulkRequest -tenantid $TenantFilter -Requests @($Requests) + + $Results = foreach ($Response in @($Responses)) { + $ResponseIndex = [int]$Response.id + $User = $Users[$ResponseIndex] + $Success = [int]$Response.status -in @(200, 204) + $ErrorMessage = if ($Response.body.error.message) { $Response.body.error.message } else { "Unknown error (Status: $($Response.status))" } + $Result = if ($Success) { "Set $User's sponsor to $Sponsor" } else { "Failed to set $User's sponsor: $ErrorMessage" } + $Severity = if ($Success) { 'Info' } else { 'Error' } + + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev $Severity + + [pscustomobject]@{ + User = $User + Sponsor = $Sponsor + Success = $Success + Result = $Result + Status = $Response.status + } + } + + return @($Results) } diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditUser.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditUser.ps1 index b6f95c20f112..417b90b22cc6 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditUser.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditUser.ps1 @@ -242,13 +242,13 @@ function Invoke-EditUser { } if ($Request.body.setManager.value) { - $ManagerResult = Set-CIPPManager -User $UserPrincipalName -Manager $Request.body.setManager.value -TenantFilter $UserObj.tenantFilter -Headers $Headers - $Results.Add($ManagerResult) + $ManagerResults = Set-CIPPManager -Users $UserPrincipalName -Manager $Request.body.setManager.value -TenantFilter $UserObj.tenantFilter -Headers $Headers + $Results.Add($ManagerResults.Result) } if ($Request.body.setSponsor.value) { - $SponsorResult = Set-CIPPSponsor -User $UserPrincipalName -Sponsor $Request.body.setSponsor.value -TenantFilter $UserObj.tenantFilter -Headers $Headers - $Results.Add($SponsorResult) + $SponsorResults = Set-CIPPSponsor -Users $UserPrincipalName -Sponsor $Request.body.setSponsor.value -TenantFilter $UserObj.tenantFilter -Headers $Headers + $Results.Add($SponsorResults.Result) } return ([HttpResponseContext]@{ diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-PatchUser.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-PatchUser.ps1 index c4c269b9c338..b71874b17ca5 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-PatchUser.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-PatchUser.ps1 @@ -14,7 +14,7 @@ function Invoke-PatchUser { $HttpResponse = [HttpResponseContext]@{ StatusCode = [HttpStatusCode]::OK - Body = @{'Results' = @("Default response, you should never see this.") } + Body = @{'Results' = @('Default response, you should never see this.') } } try { @@ -36,22 +36,37 @@ function Invoke-PatchUser { # Group users by tenant filter $UsersByTenant = $Users | Group-Object -Property tenantFilter - $TotalSuccessCount = 0 - $AllErrorMessages = @() + $TotalPatchSuccessCount = 0 + $TotalManagerSuccessCount = 0 + $TotalSponsorSuccessCount = 0 + $AllErrorMessages = [System.Collections.Generic.List[string]]::new() + $HasManagerUpdates = @($Users | Where-Object { -not [string]::IsNullOrWhiteSpace($_.manager) }).Count -gt 0 + $HasSponsorUpdates = @($Users | Where-Object { -not [string]::IsNullOrWhiteSpace($_.sponsor) }).Count -gt 0 + $HasRelationshipUpdates = $HasManagerUpdates -or $HasSponsorUpdates # Process each tenant separately foreach ($TenantGroup in $UsersByTenant) { $tenantFilter = $TenantGroup.Name $TenantUsers = $TenantGroup.Group + $UsersWithManager = $TenantUsers | Where-Object { -not [string]::IsNullOrWhiteSpace($_.manager) } + $ManagerGroups = $UsersWithManager | Group-Object -Property manager + $UsersWithSponsor = $TenantUsers | Where-Object { -not [string]::IsNullOrWhiteSpace($_.sponsor) } + $SponsorGroups = $UsersWithSponsor | Group-Object -Property sponsor # Build bulk requests for this tenant $int = 0 - $BulkRequests = foreach ($User in $TenantUsers) { - # Remove the id and tenantFilter properties from the body since they're not user properties - $PatchBody = $User | Select-Object -Property * -ExcludeProperty id, tenantFilter + $BulkRequests = [System.Collections.Generic.List[object]]::new() + $BulkRequestUsers = [System.Collections.Generic.List[object]]::new() + foreach ($User in $TenantUsers) { + # Remove routing and relationship properties from the body since they're not normal PATCH properties. + $PatchBody = $User | Select-Object -Property * -ExcludeProperty id, tenantFilter, manager, sponsor - @{ - id = $int++ + if (@($PatchBody.PSObject.Properties).Count -eq 0) { + continue + } + + $BulkRequest = @{ + id = ($int++).ToString() method = 'PATCH' url = "users/$($User.id)" body = $PatchBody @@ -59,38 +74,107 @@ function Invoke-PatchUser { 'Content-Type' = 'application/json' } } + [void]$BulkRequests.Add($BulkRequest) + [void]$BulkRequestUsers.Add($User) } # Execute bulk request for this tenant - $BulkResults = New-GraphBulkRequest -tenantid $tenantFilter -Requests @($BulkRequests) - - # Process results for this tenant - for ($i = 0; $i -lt $BulkResults.Count; $i++) { - $result = $BulkResults[$i] - $user = $TenantUsers[$i] - - if ($result.status -eq 200 -or $result.status -eq 204) { - $TotalSuccessCount++ - Write-LogMessage -headers $Headers -API $APIName -tenant $tenantFilter -message "Successfully patched user $($user.id)" -Sev 'Info' - } else { - $errorMsg = if ($result.body.error.message) { - $result.body.error.message + if ($BulkRequests.Count -gt 0) { + $BulkResults = New-GraphBulkRequest -tenantid $tenantFilter -Requests ($BulkRequests.ToArray()) + + # Process results for this tenant + foreach ($BulkResult in @($BulkResults)) { + $ResultIndex = [int]$BulkResult.id + $User = $BulkRequestUsers[$ResultIndex] + + if ($BulkResult.status -eq 200 -or $BulkResult.status -eq 204) { + $TotalPatchSuccessCount++ + Write-LogMessage -headers $Headers -API $APIName -tenant $tenantFilter -message "Successfully patched user $($User.id)" -Sev 'Info' } else { - "Unknown error (Status: $($result.status))" + $ErrorMessage = if ($BulkResult.body.error.message) { + $BulkResult.body.error.message + } else { + "Unknown error (Status: $($BulkResult.status))" + } + [void]$AllErrorMessages.Add("Failed to patch user $($User.id) in tenant $($tenantFilter): $ErrorMessage") + Write-LogMessage -headers $Headers -API $APIName -tenant $tenantFilter -message "Failed to patch user $($User.id). Error: $ErrorMessage" -Sev 'Error' + } + } + } + + foreach ($ManagerGroup in $ManagerGroups) { + $UserIds = @($ManagerGroup.Group | ForEach-Object { $_.id }) + $ManagerUpn = $ManagerGroup.Name + + try { + $ManagerResults = Set-CIPPManager -Users $UserIds -Manager $ManagerUpn -TenantFilter $tenantFilter -Headers $Headers -APIName $APIName + + foreach ($ManagerResult in @($ManagerResults)) { + if ($ManagerResult.Success) { + $TotalManagerSuccessCount++ + } else { + [void]$AllErrorMessages.Add("Failed to set manager for $($ManagerResult.User) in tenant $($tenantFilter): $($ManagerResult.Result)") + } + } + } catch { + foreach ($UserId in $UserIds) { + [void]$AllErrorMessages.Add("Failed to set manager for $UserId in tenant $($tenantFilter): $($_.Exception.Message)") + } + } + } + + foreach ($SponsorGroup in $SponsorGroups) { + $UserIds = @($SponsorGroup.Group | ForEach-Object { $_.id }) + $SponsorUpn = $SponsorGroup.Name + + try { + $SponsorResults = Set-CIPPSponsor -Users $UserIds -Sponsor $SponsorUpn -TenantFilter $tenantFilter -Headers $Headers -APIName $APIName + + foreach ($SponsorResult in @($SponsorResults)) { + if ($SponsorResult.Success) { + $TotalSponsorSuccessCount++ + } else { + [void]$AllErrorMessages.Add("Failed to set sponsor for $($SponsorResult.User) in tenant $($tenantFilter): $($SponsorResult.Result)") + } + } + } catch { + foreach ($UserId in $UserIds) { + [void]$AllErrorMessages.Add("Failed to set sponsor for $UserId in tenant $($tenantFilter): $($_.Exception.Message)") } - $AllErrorMessages += "Failed to patch user $($user.id) in tenant $($tenantFilter): $errorMsg" - Write-LogMessage -headers $Headers -API $APIName -tenant $tenantFilter -message "Failed to patch user $($user.id). Error: $errorMsg" -Sev 'Error' } } } # Build final response + $TenantCount = ($Users | Select-Object -Property tenantFilter -Unique).Count + $RelationshipResults = [System.Collections.Generic.List[string]]::new() + if ($HasManagerUpdates) { + [void]$RelationshipResults.Add("$TotalManagerSuccessCount manager assignment$(if($TotalManagerSuccessCount -ne 1){'s'})") + } + if ($HasSponsorUpdates) { + [void]$RelationshipResults.Add("$TotalSponsorSuccessCount sponsor assignment$(if($TotalSponsorSuccessCount -ne 1){'s'})") + } + $RelationshipResultMessage = [string]::Join(' and ', $RelationshipResults.ToArray()) + + $SuccessMessage = if ($HasRelationshipUpdates -and $TotalPatchSuccessCount -gt 0) { + "Successfully patched $TotalPatchSuccessCount user$(if($TotalPatchSuccessCount -ne 1){'s'}) and updated $RelationshipResultMessage across $TenantCount tenant$(if($TenantCount -ne 1){'s'})" + } elseif ($HasRelationshipUpdates) { + "Successfully updated $RelationshipResultMessage across $TenantCount tenant$(if($TenantCount -ne 1){'s'})" + } else { + "Successfully patched $TotalPatchSuccessCount user$(if($TotalPatchSuccessCount -ne 1){'s'}) across $TenantCount tenant$(if($TenantCount -ne 1){'s'})" + } + if ($AllErrorMessages.Count -eq 0) { - $TenantCount = ($Users | Select-Object -Property tenantFilter -Unique).Count - $HttpResponse.Body = @{'Results' = @("Successfully patched $TotalSuccessCount user$(if($TotalSuccessCount -ne 1){'s'}) across $TenantCount tenant$(if($TenantCount -ne 1){'s'})") } + $HttpResponse.Body = @{'Results' = @($SuccessMessage) } } else { $HttpResponse.StatusCode = [HttpStatusCode]::BadRequest - $HttpResponse.Body = @{'Results' = $AllErrorMessages + @("Successfully patched $TotalSuccessCount of $($Users.Count) users") } + $PartialSuccessMessage = if ($HasRelationshipUpdates) { $SuccessMessage } else { "Successfully patched $TotalPatchSuccessCount of $($Users.Count) users" } + $Results = [System.Collections.Generic.List[string]]::new() + foreach ($ErrorMessage in $AllErrorMessages) { + [void]$Results.Add($ErrorMessage) + } + [void]$Results.Add($PartialSuccessMessage) + $HttpResponse.Body = @{'Results' = @($Results.ToArray()) } } } @@ -100,4 +184,4 @@ function Invoke-PatchUser { } return $HttpResponse -} \ No newline at end of file +} From a8a0c9d57e4e8bc1ebf7e802325f12bdcdb328b4 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 8 May 2026 12:55:16 +0200 Subject: [PATCH 25/36] fixes #5967 --- .../Applications/Invoke-AddOfficeApp.ps1 | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddOfficeApp.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddOfficeApp.ps1 index ff880318819f..0187c47e1f53 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddOfficeApp.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddOfficeApp.ps1 @@ -19,8 +19,21 @@ function Invoke-AddOfficeApp { try { $ExistingO365 = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/deviceAppManagement/mobileApps' -tenantid $Tenant | Where-Object { $_.displayName -eq 'Microsoft 365 Apps for Windows 10 and later' } if (!$ExistingO365) { - # Check if custom XML is provided - if ($Request.Body.useCustomXml -and $Request.Body.customXml) { + # Check if this is a template deployment with IntuneBody (saved from existing app) + if ($Request.Body.IntuneBody) { + $IntuneBody = $Request.Body.IntuneBody + if ($IntuneBody -is [string]) { + $IntuneBody = $IntuneBody | ConvertFrom-Json -Depth 100 + } + # Remove read-only properties that the Graph API won't accept on create + $ReadOnlyProps = @('id', 'createdDateTime', 'lastModifiedDateTime', 'uploadState', 'publishingState', 'isAssigned', 'roleScopeTagIds', 'dependentAppCount', 'supersedingAppCount', 'supersededAppCount', 'committedContentVersion', 'fileName', 'size') + foreach ($prop in $ReadOnlyProps) { + if ($IntuneBody.PSObject.Properties[$prop]) { + $IntuneBody.PSObject.Properties.Remove($prop) + } + } + $ObjBody = $IntuneBody + } elseif ($Request.Body.useCustomXml -and $Request.Body.customXml) { # Use custom XML configuration $ObjBody = [pscustomobject]@{ '@odata.type' = '#microsoft.graph.officeSuiteApp' @@ -76,7 +89,7 @@ function Invoke-AddOfficeApp { 'officeSuiteAppDefaultFileFormat' = 'OfficeOpenXMLFormat' 'localesToInstall' = @($Request.Body.languages.value) 'shouldUninstallOlderVersionsOfOffice' = [bool]$Request.Body.RemoveVersions - 'updateChannel' = $Request.Body.updateChannel.value + 'updateChannel' = if ($Request.Body.updateChannel.value) { $Request.Body.updateChannel.value } else { $Request.Body.updateChannel } 'useSharedComputerActivation' = [bool]$Request.Body.SharedComputerActivation 'productIds' = $products 'largeIcon' = @{ From 6c440c0121ceaba7eb1b7cf7305b06b76ad95ef5 Mon Sep 17 00:00:00 2001 From: Joachim Date: Fri, 8 May 2026 13:54:24 +0200 Subject: [PATCH 26/36] Fix usageLocation autocomplete object in JIT Admin (#5910) The autocomplete component sends {label, value} objects but the Graph API expects a plain string for usageLocation. Extract .value before passing to API, matching the pattern used by other user endpoints (Invoke-AddUserDefaults, Invoke-EditUser, etc). --- .../Administration/Users/Invoke-AddJITAdminTemplate.ps1 | 4 ++-- .../Administration/Users/Invoke-EditJITAdminTemplate.ps1 | 4 ++-- .../Identity/Administration/Users/Invoke-ExecJITAdmin.ps1 | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddJITAdminTemplate.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddJITAdminTemplate.ps1 index 141a2754cf0b..0f12d80773df 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddJITAdminTemplate.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddJITAdminTemplate.ps1 @@ -112,8 +112,8 @@ function Invoke-AddJITAdminTemplate { if (![string]::IsNullOrWhiteSpace($Request.Body.defaultUserName)) { $TemplateObject.defaultUserName = $Request.Body.defaultUserName } - if (![string]::IsNullOrWhiteSpace($Request.Body.defaultUsageLocation)) { - $TemplateObject.defaultUsageLocation = $Request.Body.defaultUsageLocation + if ($Request.Body.defaultUsageLocation) { + $TemplateObject.defaultUsageLocation = $Request.Body.defaultUsageLocation.value ?? $Request.Body.defaultUsageLocation } # defaultDomain is only saved for specific tenant templates (not AllTenants) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditJITAdminTemplate.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditJITAdminTemplate.ps1 index b3282e64647b..4abb488e7f03 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditJITAdminTemplate.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditJITAdminTemplate.ps1 @@ -130,8 +130,8 @@ function Invoke-EditJITAdminTemplate { if (![string]::IsNullOrWhiteSpace($Request.Body.defaultUserName)) { $TemplateObject.defaultUserName = $Request.Body.defaultUserName } - if (![string]::IsNullOrWhiteSpace($Request.Body.defaultUsageLocation)) { - $TemplateObject.defaultUsageLocation = $Request.Body.defaultUsageLocation + if ($Request.Body.defaultUsageLocation) { + $TemplateObject.defaultUsageLocation = $Request.Body.defaultUsageLocation.value ?? $Request.Body.defaultUsageLocation } # defaultDomain is only saved for specific tenant templates (not AllTenants) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecJITAdmin.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecJITAdmin.ps1 index 2f522cc0a66d..f42a54b0baad 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecJITAdmin.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecJITAdmin.ps1 @@ -63,7 +63,7 @@ function Invoke-ExecJITAdmin { 'FirstName' = $Request.Body.FirstName 'LastName' = $Request.Body.LastName 'UserPrincipalName' = $Username - 'UsageLocation' = $Request.Body.usageLocation + 'UsageLocation' = $Request.Body.usageLocation.value ?? $Request.Body.usageLocation } Expiration = $Expiration StartDate = $Start From 3b609649a3b7ac970f51088c467ab897ba4ad2ba Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Fri, 8 May 2026 13:57:08 +0200 Subject: [PATCH 27/36] fix(jit-admin): honor TAP lifetime policy bounds Fixes https://github.com/KelvinTegelaar/CIPP/issues/5965 --- .../Public/Set-CIPPAuthenticationPolicy.ps1 | 4 +- .../Users/Invoke-ExecJITAdmin.ps1 | 40 ++++++++++++++++--- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/Modules/CIPPCore/Public/Set-CIPPAuthenticationPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPAuthenticationPolicy.ps1 index a7202202bef0..75d7971388a4 100644 --- a/Modules/CIPPCore/Public/Set-CIPPAuthenticationPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPAuthenticationPolicy.ps1 @@ -26,7 +26,7 @@ function Set-CIPPAuthenticationPolicy { } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -headers $Headers -API $APIName -tenant $Tenant -message "Could not get CurrentInfo for $AuthenticationMethodId. Error:$($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Return "Could not get CurrentInfo for $AuthenticationMethodId. Error:$($ErrorMessage.NormalizedError)" + return "Could not get CurrentInfo for $AuthenticationMethodId. Error:$($ErrorMessage.NormalizedError)" } switch ($AuthenticationMethodId) { @@ -114,7 +114,7 @@ function Set-CIPPAuthenticationPolicy { throw "Setting $AuthenticationMethodId to enabled is not allowed" } } - Default { + default { Write-LogMessage -headers $Headers -API $APIName -tenant $Tenant -message "Somehow you hit the default case with an input of $AuthenticationMethodId . You probably made a typo in the input for AuthenticationMethodId. It`'s case sensitive." -sev Error throw "Somehow you hit the default case with an input of $AuthenticationMethodId . You probably made a typo in the input for AuthenticationMethodId. It`'s case sensitive." } diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecJITAdmin.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecJITAdmin.ps1 index 2f522cc0a66d..069233e2b32d 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecJITAdmin.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecJITAdmin.ps1 @@ -129,14 +129,42 @@ function Invoke-ExecJITAdmin { #Region TAP creation if ($Request.Body.UseTAP) { try { - if ($Start -gt (Get-Date)) { - $TapParams = @{ - startDateTime = [System.DateTimeOffset]::FromUnixTimeSeconds($Request.Body.StartDate).DateTime + $LifetimeMinutes = $null + $RequestedMinutes = $null + $ParsedRequestLifetime = $false + if (![string]::IsNullOrWhiteSpace($Request.Body.tapLifetimeInMinutes)) { + try { + $RequestedMinutes = [int]$Request.Body.tapLifetimeInMinutes + $ParsedRequestLifetime = $true + } catch { + Write-Warning "Failed to parse TAP lifetime from request: $($_.Exception.Message)" + } + } + + if ($null -eq $RequestedMinutes) { + $RequestedMinutes = [int](($Expiration - $Start).TotalMinutes) + } + + try { + $Policy = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/authenticationMethodsPolicy/authenticationMethodConfigurations/TemporaryAccessPass' -tenantid $TenantFilter + $PolicyMax = [int]($Policy.maximumLifetimeInMinutes ?? 1440) + $PolicyMin = [Math]::Min([int]($Policy.minimumLifetimeInMinutes ?? 1), $PolicyMax) + $LifetimeMinutes = [Math]::Min([Math]::Max($RequestedMinutes, $PolicyMin), $PolicyMax) + } catch { + Write-Warning "Failed to determine TAP lifetime from policy: $($_.Exception.Message)" + if ($ParsedRequestLifetime) { + $LifetimeMinutes = $RequestedMinutes } - $TapBody = ConvertTo-Json -Depth 5 -InputObject $TapParams - } else { - $TapBody = '{}' } + + $TapParams = @{} + if ($Start -gt (Get-Date)) { + $TapParams.startDateTime = [System.DateTimeOffset]::FromUnixTimeSeconds($Request.Body.StartDate).DateTime + } + if ($LifetimeMinutes -gt 0) { + $TapParams.lifetimeInMinutes = [int]$LifetimeMinutes + } + $TapBody = if ($TapParams.Count) { ConvertTo-Json -Depth 5 -InputObject $TapParams } else { '{}' } # Write-Information "https://graph.microsoft.com/beta/users/$Username/authentication/temporaryAccessPassMethods" # Retry creating the TAP up to 10 times, since it can fail due to the user not being fully created yet. Sometimes it takes 2 reties, sometimes it takes 8+. Very annoying. -Bobby $Retries = 0 From 2529b6aae81fe9ae10248c74fad67c41d9188a6a Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 8 May 2026 14:07:43 +0200 Subject: [PATCH 28/36] fixes #5925 --- ...PPStandardTenantAllowBlockListTemplate.ps1 | 41 +++++++++++++++---- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTenantAllowBlockListTemplate.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTenantAllowBlockListTemplate.ps1 index 38ab5cedcbbc..c3d4522eacd2 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTenantAllowBlockListTemplate.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTenantAllowBlockListTemplate.ps1 @@ -63,16 +63,42 @@ function Invoke-CIPPStandardTenantAllowBlockListTemplate { }) if ($Settings.remediate -eq $true) { + # Track entries submitted across templates to handle overlapping entries without relying on Exchange replication + $SubmittedEntries = [System.Collections.Generic.Dictionary[string, System.Collections.Generic.HashSet[string]]]::new([System.StringComparer]::OrdinalIgnoreCase) + foreach ($TemplateData in $ResolvedTemplates) { try { $Entries = @($TemplateData.entries -split '[,;]' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | ForEach-Object { $_.Trim() }) + $ListType = [string]$TemplateData.listType + + # Get existing entries to avoid duplicate errors that block the entire batch + if (-not $SubmittedEntries.ContainsKey($ListType)) { + $SubmittedEntries[$ListType] = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) + try { + $ExistingItems = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-TenantAllowBlockListItems' -cmdParams @{ + ListType = $ListType + } + foreach ($Item in @($ExistingItems)) { + [void]$SubmittedEntries[$ListType].Add($Item.Value) + } + } catch { + # If we can't fetch existing items, continue with empty set + } + } + + $NewEntries = @($Entries | Where-Object { -not $SubmittedEntries[$ListType].Contains($_) }) + + if ($NewEntries.Count -eq 0) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "All entries from Tenant Allow/Block List template '$($TemplateData.templateName)' already exist for $Tenant" -sev 'Info' + continue + } $ExoParams = @{ tenantid = $Tenant cmdlet = 'New-TenantAllowBlockListItems' cmdParams = @{ - Entries = $Entries - ListType = [string]$TemplateData.listType + Entries = $NewEntries + ListType = $ListType Notes = [string]$TemplateData.notes $TemplateData.listMethod = [bool]$true } @@ -85,14 +111,13 @@ function Invoke-CIPPStandardTenantAllowBlockListTemplate { } New-ExoRequest @ExoParams - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully deployed Tenant Allow/Block List template '$($TemplateData.templateName)' with entries: $($TemplateData.entries)" -sev 'Info' + foreach ($Entry in $NewEntries) { + [void]$SubmittedEntries[$ListType].Add($Entry) + } + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully deployed Tenant Allow/Block List template '$($TemplateData.templateName)' with entries: $($NewEntries -join ', ')" -sev 'Info' } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - if ($ErrorMessage -like '*already exists*') { - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Tenant Allow/Block List entries from template '$($TemplateData.templateName)' already exist for $Tenant" -sev 'Info' - } else { - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to deploy Tenant Allow/Block List template '$($TemplateData.templateName)' for $Tenant. Error: $ErrorMessage" -sev 'Error' - } + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to deploy Tenant Allow/Block List template '$($TemplateData.templateName)' for $Tenant. Error: $ErrorMessage" -sev 'Error' } } } From 74124f4f4a3b81958217bbf22f9a0c26c218ac22 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 8 May 2026 09:51:00 -0400 Subject: [PATCH 29/36] chore: update DNSHealth to 1.1.7 --- .../DNSHealth/{1.1.6 => 1.1.7}/DNSHealth.psd1 | 2 +- .../DNSHealth/{1.1.6 => 1.1.7}/DNSHealth.psm1 | 0 .../MailProviders/AppRiver.json | 0 .../MailProviders/BarracudaESS.json | 0 .../MailProviders/Google.json | 0 .../MailProviders/HornetSecurity.json | 0 .../MailProviders/Intermedia.json | 0 .../MailProviders/Microsoft365.json | 0 .../MailProviders/Mimecast.json | 0 .../{1.1.6 => 1.1.7}/MailProviders/Null.json | 0 .../MailProviders/Proofpoint.json | 0 .../MailProviders/Reflexion.json | 0 .../MailProviders/Sophos.json | 0 .../MailProviders/SpamTitan.json | 0 .../MailProviders/SymantecCloud.json | 0 .../MailProviders/_template.json | 0 .../{1.1.6 => 1.1.7}/PSGetModuleInfo.xml | 38 +++++++++---------- 17 files changed, 20 insertions(+), 20 deletions(-) rename Modules/DNSHealth/{1.1.6 => 1.1.7}/DNSHealth.psd1 (99%) rename Modules/DNSHealth/{1.1.6 => 1.1.7}/DNSHealth.psm1 (100%) rename Modules/DNSHealth/{1.1.6 => 1.1.7}/MailProviders/AppRiver.json (100%) rename Modules/DNSHealth/{1.1.6 => 1.1.7}/MailProviders/BarracudaESS.json (100%) rename Modules/DNSHealth/{1.1.6 => 1.1.7}/MailProviders/Google.json (100%) rename Modules/DNSHealth/{1.1.6 => 1.1.7}/MailProviders/HornetSecurity.json (100%) rename Modules/DNSHealth/{1.1.6 => 1.1.7}/MailProviders/Intermedia.json (100%) rename Modules/DNSHealth/{1.1.6 => 1.1.7}/MailProviders/Microsoft365.json (100%) rename Modules/DNSHealth/{1.1.6 => 1.1.7}/MailProviders/Mimecast.json (100%) rename Modules/DNSHealth/{1.1.6 => 1.1.7}/MailProviders/Null.json (100%) rename Modules/DNSHealth/{1.1.6 => 1.1.7}/MailProviders/Proofpoint.json (100%) rename Modules/DNSHealth/{1.1.6 => 1.1.7}/MailProviders/Reflexion.json (100%) rename Modules/DNSHealth/{1.1.6 => 1.1.7}/MailProviders/Sophos.json (100%) rename Modules/DNSHealth/{1.1.6 => 1.1.7}/MailProviders/SpamTitan.json (100%) rename Modules/DNSHealth/{1.1.6 => 1.1.7}/MailProviders/SymantecCloud.json (100%) rename Modules/DNSHealth/{1.1.6 => 1.1.7}/MailProviders/_template.json (100%) rename Modules/DNSHealth/{1.1.6 => 1.1.7}/PSGetModuleInfo.xml (85%) diff --git a/Modules/DNSHealth/1.1.6/DNSHealth.psd1 b/Modules/DNSHealth/1.1.7/DNSHealth.psd1 similarity index 99% rename from Modules/DNSHealth/1.1.6/DNSHealth.psd1 rename to Modules/DNSHealth/1.1.7/DNSHealth.psd1 index 92d590ea5bcf..be713cdf7187 100644 --- a/Modules/DNSHealth/1.1.6/DNSHealth.psd1 +++ b/Modules/DNSHealth/1.1.7/DNSHealth.psd1 @@ -12,7 +12,7 @@ RootModule = 'DNSHealth.psm1' # Version number of this module. - ModuleVersion = '1.1.6' + ModuleVersion = '1.1.7' # Supported PSEditions # CompatiblePSEditions = @() diff --git a/Modules/DNSHealth/1.1.6/DNSHealth.psm1 b/Modules/DNSHealth/1.1.7/DNSHealth.psm1 similarity index 100% rename from Modules/DNSHealth/1.1.6/DNSHealth.psm1 rename to Modules/DNSHealth/1.1.7/DNSHealth.psm1 diff --git a/Modules/DNSHealth/1.1.6/MailProviders/AppRiver.json b/Modules/DNSHealth/1.1.7/MailProviders/AppRiver.json similarity index 100% rename from Modules/DNSHealth/1.1.6/MailProviders/AppRiver.json rename to Modules/DNSHealth/1.1.7/MailProviders/AppRiver.json diff --git a/Modules/DNSHealth/1.1.6/MailProviders/BarracudaESS.json b/Modules/DNSHealth/1.1.7/MailProviders/BarracudaESS.json similarity index 100% rename from Modules/DNSHealth/1.1.6/MailProviders/BarracudaESS.json rename to Modules/DNSHealth/1.1.7/MailProviders/BarracudaESS.json diff --git a/Modules/DNSHealth/1.1.6/MailProviders/Google.json b/Modules/DNSHealth/1.1.7/MailProviders/Google.json similarity index 100% rename from Modules/DNSHealth/1.1.6/MailProviders/Google.json rename to Modules/DNSHealth/1.1.7/MailProviders/Google.json diff --git a/Modules/DNSHealth/1.1.6/MailProviders/HornetSecurity.json b/Modules/DNSHealth/1.1.7/MailProviders/HornetSecurity.json similarity index 100% rename from Modules/DNSHealth/1.1.6/MailProviders/HornetSecurity.json rename to Modules/DNSHealth/1.1.7/MailProviders/HornetSecurity.json diff --git a/Modules/DNSHealth/1.1.6/MailProviders/Intermedia.json b/Modules/DNSHealth/1.1.7/MailProviders/Intermedia.json similarity index 100% rename from Modules/DNSHealth/1.1.6/MailProviders/Intermedia.json rename to Modules/DNSHealth/1.1.7/MailProviders/Intermedia.json diff --git a/Modules/DNSHealth/1.1.6/MailProviders/Microsoft365.json b/Modules/DNSHealth/1.1.7/MailProviders/Microsoft365.json similarity index 100% rename from Modules/DNSHealth/1.1.6/MailProviders/Microsoft365.json rename to Modules/DNSHealth/1.1.7/MailProviders/Microsoft365.json diff --git a/Modules/DNSHealth/1.1.6/MailProviders/Mimecast.json b/Modules/DNSHealth/1.1.7/MailProviders/Mimecast.json similarity index 100% rename from Modules/DNSHealth/1.1.6/MailProviders/Mimecast.json rename to Modules/DNSHealth/1.1.7/MailProviders/Mimecast.json diff --git a/Modules/DNSHealth/1.1.6/MailProviders/Null.json b/Modules/DNSHealth/1.1.7/MailProviders/Null.json similarity index 100% rename from Modules/DNSHealth/1.1.6/MailProviders/Null.json rename to Modules/DNSHealth/1.1.7/MailProviders/Null.json diff --git a/Modules/DNSHealth/1.1.6/MailProviders/Proofpoint.json b/Modules/DNSHealth/1.1.7/MailProviders/Proofpoint.json similarity index 100% rename from Modules/DNSHealth/1.1.6/MailProviders/Proofpoint.json rename to Modules/DNSHealth/1.1.7/MailProviders/Proofpoint.json diff --git a/Modules/DNSHealth/1.1.6/MailProviders/Reflexion.json b/Modules/DNSHealth/1.1.7/MailProviders/Reflexion.json similarity index 100% rename from Modules/DNSHealth/1.1.6/MailProviders/Reflexion.json rename to Modules/DNSHealth/1.1.7/MailProviders/Reflexion.json diff --git a/Modules/DNSHealth/1.1.6/MailProviders/Sophos.json b/Modules/DNSHealth/1.1.7/MailProviders/Sophos.json similarity index 100% rename from Modules/DNSHealth/1.1.6/MailProviders/Sophos.json rename to Modules/DNSHealth/1.1.7/MailProviders/Sophos.json diff --git a/Modules/DNSHealth/1.1.6/MailProviders/SpamTitan.json b/Modules/DNSHealth/1.1.7/MailProviders/SpamTitan.json similarity index 100% rename from Modules/DNSHealth/1.1.6/MailProviders/SpamTitan.json rename to Modules/DNSHealth/1.1.7/MailProviders/SpamTitan.json diff --git a/Modules/DNSHealth/1.1.6/MailProviders/SymantecCloud.json b/Modules/DNSHealth/1.1.7/MailProviders/SymantecCloud.json similarity index 100% rename from Modules/DNSHealth/1.1.6/MailProviders/SymantecCloud.json rename to Modules/DNSHealth/1.1.7/MailProviders/SymantecCloud.json diff --git a/Modules/DNSHealth/1.1.6/MailProviders/_template.json b/Modules/DNSHealth/1.1.7/MailProviders/_template.json similarity index 100% rename from Modules/DNSHealth/1.1.6/MailProviders/_template.json rename to Modules/DNSHealth/1.1.7/MailProviders/_template.json diff --git a/Modules/DNSHealth/1.1.6/PSGetModuleInfo.xml b/Modules/DNSHealth/1.1.7/PSGetModuleInfo.xml similarity index 85% rename from Modules/DNSHealth/1.1.6/PSGetModuleInfo.xml rename to Modules/DNSHealth/1.1.7/PSGetModuleInfo.xml index 0a8245a31c45..441517b7b04a 100644 --- a/Modules/DNSHealth/1.1.6/PSGetModuleInfo.xml +++ b/Modules/DNSHealth/1.1.7/PSGetModuleInfo.xml @@ -7,13 +7,13 @@ DNSHealth - 1.1.6 + 1.1.7 Module CIPP DNS Health Check Module John Duprey johnduprey 2023 John Duprey -
2026-04-24T17:42:26-04:00
+
2026-05-08T13:48:38-04:00
@@ -36,18 +36,14 @@ - Cmdlet + Workflow - RoleCapability - - - - Command + Function @@ -72,15 +68,15 @@ - DscResource + RoleCapability - Workflow + DscResource - Function + Command @@ -104,6 +100,10 @@ + + Cmdlet + + @@ -127,24 +127,24 @@ True True 0 - 477 + 491 31557 - 4/24/2026 5:42:26 PM -04:00 - 4/24/2026 5:42:26 PM -04:00 - 4/24/2026 5:42:26 PM -04:00 + 5/8/2026 1:48:38 PM -04:00 + 5/8/2026 1:48:38 PM -04:00 + 5/8/2026 1:48:38 PM -04:00 PSModule PSFunction_Read-DmarcPolicy PSCommand_Read-DmarcPolicy PSFunction_Read-MtaStsPolicy PSCommand_Read-MtaStsPolicy PSFunction_Add-MailProvider PSCommand_Add-MailProvider PSFunction_Get-MailProvider PSCommand_Get-MailProvider PSFunction_Read-DkimRecord PSCommand_Read-DkimRecord PSFunction_Read-MtaStsRecord PSCommand_Read-MtaStsRecord PSFunction_Read-MXRecord PSCommand_Read-MXRecord PSFunction_Read-NSRecord PSCommand_Read-NSRecord PSFunction_Read-SPFRecord PSCommand_Read-SPFRecord PSFunction_Read-TlsRptRecord PSCommand_Read-TlsRptRecord PSFunction_Read-WhoisRecord PSCommand_Read-WhoisRecord PSFunction_Remove-MailProvider PSCommand_Remove-MailProvider PSFunction_Resolve-DnsHttpsQuery PSCommand_Resolve-DnsHttpsQuery PSFunction_Set-DnsResolver PSCommand_Set-DnsResolver PSFunction_Test-DNSSEC PSCommand_Test-DNSSEC PSFunction_Test-HttpsCertificate PSCommand_Test-HttpsCertificate PSFunction_Test-MtaSts PSCommand_Test-MtaSts PSIncludes_Function False - 2026-04-24T17:42:26Z - 1.1.6 + 2026-05-08T13:48:38Z + 1.1.7 John Duprey false Module - DNSHealth.nuspec|MailProviders\Microsoft365.json|MailProviders\Sophos.json|DNSHealth.psd1|MailProviders\SymantecCloud.json|MailProviders\SpamTitan.json|MailProviders\AppRiver.json|DNSHealth.psm1|MailProviders\Intermedia.json|MailProviders\_template.json|MailProviders\BarracudaESS.json|MailProviders\Reflexion.json|MailProviders\HornetSecurity.json|MailProviders\Google.json|MailProviders\Proofpoint.json|MailProviders\Null.json|MailProviders\Mimecast.json + DNSHealth.nuspec|MailProviders\SymantecCloud.json|MailProviders\Microsoft365.json|MailProviders\Sophos.json|DNSHealth.psd1|MailProviders\Intermedia.json|MailProviders\SpamTitan.json|MailProviders\AppRiver.json|DNSHealth.psm1|MailProviders\Reflexion.json|MailProviders\_template.json|MailProviders\BarracudaESS.json|MailProviders\Null.json|MailProviders\HornetSecurity.json|MailProviders\Google.json|MailProviders\Proofpoint.json|MailProviders\Mimecast.json a300d2b0-d468-46d1-88a3-e442a76b655b 7.0
- /Users/johnduprey/GitHub/CIPP Workspace/CIPP-API/Modules/DNSHealth/1.1.6 + /Users/johnduprey/GitHub/CIPP Workspace/CIPP-API/Modules/DNSHealth/1.1.7 From 176aa964d6eeca842fdab25746f639236fd4470d Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 8 May 2026 11:17:17 -0400 Subject: [PATCH 30/36] fix: sharing capability based on desired state for file requests --- .../Public/Standards/Invoke-CIPPStandardSPFileRequests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPFileRequests.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPFileRequests.ps1 index 11cba32e784f..95b4c14ac299 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPFileRequests.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPFileRequests.ps1 @@ -166,7 +166,7 @@ function Invoke-CIPPStandardSPFileRequests { OneDriveRequestFilesLinkEnabled = $WantedState CoreRequestFilesLinkExpirationInDays = if ($null -ne $ExpirationDays -and $WantedState -eq $true) { $ExpirationDays } else { $null } OneDriveRequestFilesLinkExpirationInDays = if ($null -ne $ExpirationDays -and $WantedState -eq $true) { $ExpirationDays } else { $null } - SharingCapability = 'External Users and Guests (Anyone)' + SharingCapability = if ($WantedState -eq $true) { 'External Users and Guests (Anyone)' } else { $SharingCapabilityEnum[$CurrentState.SharingCapability] } } Set-CIPPStandardsCompareField -FieldName 'standards.SPFileRequests' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } From f105398cf41da36b04d74b2fb3cc27db6eaf5c48 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 8 May 2026 11:47:42 -0400 Subject: [PATCH 31/36] chore: update dnshealth to 1.1.8 --- .../DNSHealth/{1.1.7 => 1.1.8}/DNSHealth.psd1 | 4 +- .../DNSHealth/{1.1.7 => 1.1.8}/DNSHealth.psm1 | 106 ++++++++++++++++++ .../MailProviders/AppRiver.json | 0 .../MailProviders/BarracudaESS.json | 0 .../MailProviders/Google.json | 0 .../MailProviders/HornetSecurity.json | 0 .../MailProviders/Intermedia.json | 0 .../MailProviders/Microsoft365.json | 0 .../MailProviders/Mimecast.json | 0 .../{1.1.7 => 1.1.8}/MailProviders/Null.json | 0 .../MailProviders/Proofpoint.json | 0 .../MailProviders/Reflexion.json | 0 .../MailProviders/Sophos.json | 0 .../MailProviders/SpamTitan.json | 0 .../MailProviders/SymantecCloud.json | 0 .../MailProviders/_template.json | 0 .../{1.1.7 => 1.1.8}/PSGetModuleInfo.xml | 24 ++-- 17 files changed, 121 insertions(+), 13 deletions(-) rename Modules/DNSHealth/{1.1.7 => 1.1.8}/DNSHealth.psd1 (92%) rename Modules/DNSHealth/{1.1.7 => 1.1.8}/DNSHealth.psm1 (96%) rename Modules/DNSHealth/{1.1.7 => 1.1.8}/MailProviders/AppRiver.json (100%) rename Modules/DNSHealth/{1.1.7 => 1.1.8}/MailProviders/BarracudaESS.json (100%) rename Modules/DNSHealth/{1.1.7 => 1.1.8}/MailProviders/Google.json (100%) rename Modules/DNSHealth/{1.1.7 => 1.1.8}/MailProviders/HornetSecurity.json (100%) rename Modules/DNSHealth/{1.1.7 => 1.1.8}/MailProviders/Intermedia.json (100%) rename Modules/DNSHealth/{1.1.7 => 1.1.8}/MailProviders/Microsoft365.json (100%) rename Modules/DNSHealth/{1.1.7 => 1.1.8}/MailProviders/Mimecast.json (100%) rename Modules/DNSHealth/{1.1.7 => 1.1.8}/MailProviders/Null.json (100%) rename Modules/DNSHealth/{1.1.7 => 1.1.8}/MailProviders/Proofpoint.json (100%) rename Modules/DNSHealth/{1.1.7 => 1.1.8}/MailProviders/Reflexion.json (100%) rename Modules/DNSHealth/{1.1.7 => 1.1.8}/MailProviders/Sophos.json (100%) rename Modules/DNSHealth/{1.1.7 => 1.1.8}/MailProviders/SpamTitan.json (100%) rename Modules/DNSHealth/{1.1.7 => 1.1.8}/MailProviders/SymantecCloud.json (100%) rename Modules/DNSHealth/{1.1.7 => 1.1.8}/MailProviders/_template.json (100%) rename Modules/DNSHealth/{1.1.7 => 1.1.8}/PSGetModuleInfo.xml (79%) diff --git a/Modules/DNSHealth/1.1.7/DNSHealth.psd1 b/Modules/DNSHealth/1.1.8/DNSHealth.psd1 similarity index 92% rename from Modules/DNSHealth/1.1.7/DNSHealth.psd1 rename to Modules/DNSHealth/1.1.8/DNSHealth.psd1 index be713cdf7187..a9d8c70eac4b 100644 --- a/Modules/DNSHealth/1.1.7/DNSHealth.psd1 +++ b/Modules/DNSHealth/1.1.8/DNSHealth.psd1 @@ -12,7 +12,7 @@ RootModule = 'DNSHealth.psm1' # Version number of this module. - ModuleVersion = '1.1.7' + ModuleVersion = '1.1.8' # Supported PSEditions # CompatiblePSEditions = @() @@ -69,7 +69,7 @@ # NestedModules = @() # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. - FunctionsToExport = @('Read-DmarcPolicy','Read-MtaStsPolicy','Add-MailProvider','Get-MailProvider','Read-DkimRecord','Read-MtaStsRecord','Read-MXRecord','Read-NSRecord','Read-SPFRecord','Read-TlsRptRecord','Read-WhoisRecord','Remove-MailProvider','Resolve-DnsHttpsQuery','Set-DnsResolver','Test-DNSSEC','Test-HttpsCertificate','Test-MtaSts') + FunctionsToExport = @('Read-DmarcPolicy','Read-MtaStsPolicy','Add-MailProvider','Get-MailProvider','Read-AutoDiscoverRecord','Read-DkimRecord','Read-MtaStsRecord','Read-MXRecord','Read-NSRecord','Read-SPFRecord','Read-TlsRptRecord','Read-WhoisRecord','Remove-MailProvider','Resolve-DnsHttpsQuery','Set-DnsResolver','Test-DNSSEC','Test-HttpsCertificate','Test-MtaSts') # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. #CmdletsToExport = '*' diff --git a/Modules/DNSHealth/1.1.7/DNSHealth.psm1 b/Modules/DNSHealth/1.1.8/DNSHealth.psm1 similarity index 96% rename from Modules/DNSHealth/1.1.7/DNSHealth.psm1 rename to Modules/DNSHealth/1.1.8/DNSHealth.psm1 index e299b961092a..113996bb69e9 100644 --- a/Modules/DNSHealth/1.1.7/DNSHealth.psm1 +++ b/Modules/DNSHealth/1.1.8/DNSHealth.psm1 @@ -923,6 +923,112 @@ function Get-MailProvider { return $Providers } #EndRegion './Public/Records/Get-MailProvider.ps1' 95 +#Region './Public/Records/Read-AutoDiscoverRecord.ps1' -1 + +function Read-AutoDiscoverRecord { + <# + .SYNOPSIS + Check AutoDiscover DNS records for a domain + + .DESCRIPTION + Resolves AutoDiscover DNS records (CNAME, A, and SRV) and validates the configuration. + For Microsoft 365, the expected setup is a CNAME pointing to autodiscover.outlook.com. + + .PARAMETER Domain + Domain to check AutoDiscover records for + + .PARAMETER ExpectedTarget + Expected CNAME/SRV target to validate against. Defaults to autodiscover.outlook.com + + .EXAMPLE + PS> Read-AutoDiscoverRecord -Domain example.com + + .EXAMPLE + PS> Read-AutoDiscoverRecord -Domain example.com -ExpectedTarget 'autodiscover.custom.com' + + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Domain, + + [Parameter(Mandatory = $false)] + [string]$ExpectedTarget = 'autodiscover.outlook.com' + ) + + $AutoDiscoverDomain = "autodiscover.$Domain" + $SrvDomain = "_autodiscover._tcp.$Domain" + + $ValidationPasses = [System.Collections.Generic.List[string]]::new() + $ValidationWarns = [System.Collections.Generic.List[string]]::new() + $ValidationFails = [System.Collections.Generic.List[string]]::new() + + $CnameRecord = $null + $ARecord = $null + $SrvRecord = $null + $RecordType = 'None' + + # Query autodiscover hostname - A query returns CNAME in chain if present + $DnsResult = Resolve-DnsHttpsQuery -Domain $AutoDiscoverDomain + if ($DnsResult.Answer) { + $CnameRecord = ($DnsResult.Answer | Where-Object { $_.type -eq 5 }).data -replace '\.$' + if ($CnameRecord) { + $RecordType = 'CNAME' + if ($CnameRecord -eq $ExpectedTarget) { + $ValidationPasses.Add("AutoDiscover CNAME correctly points to $ExpectedTarget") | Out-Null + } else { + $ValidationWarns.Add("AutoDiscover CNAME points to $CnameRecord (expected $ExpectedTarget)") | Out-Null + } + } else { + $ARecord = @(($DnsResult.Answer | Where-Object { $_.type -eq 1 }).data) + if ($ARecord.Count -gt 0) { + $RecordType = 'A' + $ValidationWarns.Add("AutoDiscover is configured with an A record ($($ARecord -join ', ')) instead of the recommended CNAME to $ExpectedTarget") | Out-Null + } + } + } + + # Check SRV record + $SrvResult = Resolve-DnsHttpsQuery -Domain $SrvDomain -RecordType SRV + if ($SrvResult.Answer) { + $SrvData = ($SrvResult.Answer | Where-Object { $_.type -eq 33 }).data + if ($SrvData) { + # SRV data format: priority weight port target + $SrvParts = $SrvData -split '\s+' + $SrvTarget = if ($SrvParts.Count -ge 4) { $SrvParts[3] -replace '\.$' } else { $SrvData -replace '\.$' } + $SrvRecord = $SrvTarget + + if ($RecordType -eq 'None') { + $RecordType = 'SRV' + } + + if ($SrvTarget -eq $ExpectedTarget) { + $ValidationPasses.Add("AutoDiscover SRV record correctly points to $ExpectedTarget") | Out-Null + } else { + $ValidationWarns.Add("AutoDiscover SRV record points to $SrvTarget (expected $ExpectedTarget)") | Out-Null + } + } + } + + # No records found at all + if (-not $CnameRecord -and ($null -eq $ARecord -or $ARecord.Count -eq 0) -and -not $SrvRecord) { + $ValidationFails.Add("No AutoDiscover DNS records found (checked CNAME, A, and SRV for $Domain)") | Out-Null + } + + [PSCustomObject]@{ + Domain = $AutoDiscoverDomain + Record = if ($CnameRecord) { $CnameRecord } elseif ($ARecord) { $ARecord -join ', ' } elseif ($SrvRecord) { $SrvRecord } else { $null } + RecordType = $RecordType + CnameRecord = $CnameRecord + ARecord = $ARecord + SrvRecord = $SrvRecord + ExpectedTarget = $ExpectedTarget + ValidationPasses = @($ValidationPasses) + ValidationWarns = @($ValidationWarns) + ValidationFails = @($ValidationFails) + } +} +#EndRegion './Public/Records/Read-AutoDiscoverRecord.ps1' 104 #Region './Public/Records/Read-DkimRecord.ps1' -1 function Read-DkimRecord { diff --git a/Modules/DNSHealth/1.1.7/MailProviders/AppRiver.json b/Modules/DNSHealth/1.1.8/MailProviders/AppRiver.json similarity index 100% rename from Modules/DNSHealth/1.1.7/MailProviders/AppRiver.json rename to Modules/DNSHealth/1.1.8/MailProviders/AppRiver.json diff --git a/Modules/DNSHealth/1.1.7/MailProviders/BarracudaESS.json b/Modules/DNSHealth/1.1.8/MailProviders/BarracudaESS.json similarity index 100% rename from Modules/DNSHealth/1.1.7/MailProviders/BarracudaESS.json rename to Modules/DNSHealth/1.1.8/MailProviders/BarracudaESS.json diff --git a/Modules/DNSHealth/1.1.7/MailProviders/Google.json b/Modules/DNSHealth/1.1.8/MailProviders/Google.json similarity index 100% rename from Modules/DNSHealth/1.1.7/MailProviders/Google.json rename to Modules/DNSHealth/1.1.8/MailProviders/Google.json diff --git a/Modules/DNSHealth/1.1.7/MailProviders/HornetSecurity.json b/Modules/DNSHealth/1.1.8/MailProviders/HornetSecurity.json similarity index 100% rename from Modules/DNSHealth/1.1.7/MailProviders/HornetSecurity.json rename to Modules/DNSHealth/1.1.8/MailProviders/HornetSecurity.json diff --git a/Modules/DNSHealth/1.1.7/MailProviders/Intermedia.json b/Modules/DNSHealth/1.1.8/MailProviders/Intermedia.json similarity index 100% rename from Modules/DNSHealth/1.1.7/MailProviders/Intermedia.json rename to Modules/DNSHealth/1.1.8/MailProviders/Intermedia.json diff --git a/Modules/DNSHealth/1.1.7/MailProviders/Microsoft365.json b/Modules/DNSHealth/1.1.8/MailProviders/Microsoft365.json similarity index 100% rename from Modules/DNSHealth/1.1.7/MailProviders/Microsoft365.json rename to Modules/DNSHealth/1.1.8/MailProviders/Microsoft365.json diff --git a/Modules/DNSHealth/1.1.7/MailProviders/Mimecast.json b/Modules/DNSHealth/1.1.8/MailProviders/Mimecast.json similarity index 100% rename from Modules/DNSHealth/1.1.7/MailProviders/Mimecast.json rename to Modules/DNSHealth/1.1.8/MailProviders/Mimecast.json diff --git a/Modules/DNSHealth/1.1.7/MailProviders/Null.json b/Modules/DNSHealth/1.1.8/MailProviders/Null.json similarity index 100% rename from Modules/DNSHealth/1.1.7/MailProviders/Null.json rename to Modules/DNSHealth/1.1.8/MailProviders/Null.json diff --git a/Modules/DNSHealth/1.1.7/MailProviders/Proofpoint.json b/Modules/DNSHealth/1.1.8/MailProviders/Proofpoint.json similarity index 100% rename from Modules/DNSHealth/1.1.7/MailProviders/Proofpoint.json rename to Modules/DNSHealth/1.1.8/MailProviders/Proofpoint.json diff --git a/Modules/DNSHealth/1.1.7/MailProviders/Reflexion.json b/Modules/DNSHealth/1.1.8/MailProviders/Reflexion.json similarity index 100% rename from Modules/DNSHealth/1.1.7/MailProviders/Reflexion.json rename to Modules/DNSHealth/1.1.8/MailProviders/Reflexion.json diff --git a/Modules/DNSHealth/1.1.7/MailProviders/Sophos.json b/Modules/DNSHealth/1.1.8/MailProviders/Sophos.json similarity index 100% rename from Modules/DNSHealth/1.1.7/MailProviders/Sophos.json rename to Modules/DNSHealth/1.1.8/MailProviders/Sophos.json diff --git a/Modules/DNSHealth/1.1.7/MailProviders/SpamTitan.json b/Modules/DNSHealth/1.1.8/MailProviders/SpamTitan.json similarity index 100% rename from Modules/DNSHealth/1.1.7/MailProviders/SpamTitan.json rename to Modules/DNSHealth/1.1.8/MailProviders/SpamTitan.json diff --git a/Modules/DNSHealth/1.1.7/MailProviders/SymantecCloud.json b/Modules/DNSHealth/1.1.8/MailProviders/SymantecCloud.json similarity index 100% rename from Modules/DNSHealth/1.1.7/MailProviders/SymantecCloud.json rename to Modules/DNSHealth/1.1.8/MailProviders/SymantecCloud.json diff --git a/Modules/DNSHealth/1.1.7/MailProviders/_template.json b/Modules/DNSHealth/1.1.8/MailProviders/_template.json similarity index 100% rename from Modules/DNSHealth/1.1.7/MailProviders/_template.json rename to Modules/DNSHealth/1.1.8/MailProviders/_template.json diff --git a/Modules/DNSHealth/1.1.7/PSGetModuleInfo.xml b/Modules/DNSHealth/1.1.8/PSGetModuleInfo.xml similarity index 79% rename from Modules/DNSHealth/1.1.7/PSGetModuleInfo.xml rename to Modules/DNSHealth/1.1.8/PSGetModuleInfo.xml index 441517b7b04a..a5693b78886a 100644 --- a/Modules/DNSHealth/1.1.7/PSGetModuleInfo.xml +++ b/Modules/DNSHealth/1.1.8/PSGetModuleInfo.xml @@ -7,13 +7,13 @@ DNSHealth - 1.1.7 + 1.1.8 Module CIPP DNS Health Check Module John Duprey johnduprey 2023 John Duprey -
2026-05-08T13:48:38-04:00
+
2026-05-08T15:46:09-04:00
@@ -51,6 +51,7 @@ Read-MtaStsPolicy Add-MailProvider Get-MailProvider + Read-AutoDiscoverRecord Read-DkimRecord Read-MtaStsRecord Read-MXRecord @@ -84,6 +85,7 @@ Read-MtaStsPolicy Add-MailProvider Get-MailProvider + Read-AutoDiscoverRecord Read-DkimRecord Read-MtaStsRecord Read-MXRecord @@ -127,15 +129,15 @@ True True 0 - 491 - 31557 - 5/8/2026 1:48:38 PM -04:00 - 5/8/2026 1:48:38 PM -04:00 - 5/8/2026 1:48:38 PM -04:00 - PSModule PSFunction_Read-DmarcPolicy PSCommand_Read-DmarcPolicy PSFunction_Read-MtaStsPolicy PSCommand_Read-MtaStsPolicy PSFunction_Add-MailProvider PSCommand_Add-MailProvider PSFunction_Get-MailProvider PSCommand_Get-MailProvider PSFunction_Read-DkimRecord PSCommand_Read-DkimRecord PSFunction_Read-MtaStsRecord PSCommand_Read-MtaStsRecord PSFunction_Read-MXRecord PSCommand_Read-MXRecord PSFunction_Read-NSRecord PSCommand_Read-NSRecord PSFunction_Read-SPFRecord PSCommand_Read-SPFRecord PSFunction_Read-TlsRptRecord PSCommand_Read-TlsRptRecord PSFunction_Read-WhoisRecord PSCommand_Read-WhoisRecord PSFunction_Remove-MailProvider PSCommand_Remove-MailProvider PSFunction_Resolve-DnsHttpsQuery PSCommand_Resolve-DnsHttpsQuery PSFunction_Set-DnsResolver PSCommand_Set-DnsResolver PSFunction_Test-DNSSEC PSCommand_Test-DNSSEC PSFunction_Test-HttpsCertificate PSCommand_Test-HttpsCertificate PSFunction_Test-MtaSts PSCommand_Test-MtaSts PSIncludes_Function + 495 + 32431 + 5/8/2026 3:46:09 PM -04:00 + 5/8/2026 3:46:09 PM -04:00 + 5/8/2026 3:46:09 PM -04:00 + PSModule PSFunction_Read-DmarcPolicy PSCommand_Read-DmarcPolicy PSFunction_Read-MtaStsPolicy PSCommand_Read-MtaStsPolicy PSFunction_Add-MailProvider PSCommand_Add-MailProvider PSFunction_Get-MailProvider PSCommand_Get-MailProvider PSFunction_Read-AutoDiscoverRecord PSCommand_Read-AutoDiscoverRecord PSFunction_Read-DkimRecord PSCommand_Read-DkimRecord PSFunction_Read-MtaStsRecord PSCommand_Read-MtaStsRecord PSFunction_Read-MXRecord PSCommand_Read-MXRecord PSFunction_Read-NSRecord PSCommand_Read-NSRecord PSFunction_Read-SPFRecord PSCommand_Read-SPFRecord PSFunction_Read-TlsRptRecord PSCommand_Read-TlsRptRecord PSFunction_Read-WhoisRecord PSCommand_Read-WhoisRecord PSFunction_Remove-MailProvider PSCommand_Remove-MailProvider PSFunction_Resolve-DnsHttpsQuery PSCommand_Resolve-DnsHttpsQuery PSFunction_Set-DnsResolver PSCommand_Set-DnsResolver PSFunction_Test-DNSSEC PSCommand_Test-DNSSEC PSFunction_Test-HttpsCertificate PSCommand_Test-HttpsCertificate PSFunction_Test-MtaSts PSCommand_Test-MtaSts PSIncludes_Function False - 2026-05-08T13:48:38Z - 1.1.7 + 2026-05-08T15:46:09Z + 1.1.8 John Duprey false Module @@ -144,7 +146,7 @@ 7.0
- /Users/johnduprey/GitHub/CIPP Workspace/CIPP-API/Modules/DNSHealth/1.1.7 + /Users/johnduprey/GitHub/CIPP Workspace/CIPP-API/Modules/DNSHealth/1.1.8 From d9c6203983e5a30d877bc8cccb318866bffc209e Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 8 May 2026 12:59:37 -0400 Subject: [PATCH 32/36] fix: update expiration days logic for SharePoint and OneDrive file requests --- .../Public/Standards/Invoke-CIPPStandardSPFileRequests.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPFileRequests.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPFileRequests.ps1 index 95b4c14ac299..e61f462f717e 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPFileRequests.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPFileRequests.ps1 @@ -164,8 +164,8 @@ function Invoke-CIPPStandardSPFileRequests { $ExpectedValue = @{ CoreRequestFilesLinkEnabled = $WantedState OneDriveRequestFilesLinkEnabled = $WantedState - CoreRequestFilesLinkExpirationInDays = if ($null -ne $ExpirationDays -and $WantedState -eq $true) { $ExpirationDays } else { $null } - OneDriveRequestFilesLinkExpirationInDays = if ($null -ne $ExpirationDays -and $WantedState -eq $true) { $ExpirationDays } else { $null } + CoreRequestFilesLinkExpirationInDays = if ($null -ne $ExpirationDays) { $ExpirationDays } else { $CurrentState.CoreRequestFilesLinkExpirationInDays } + OneDriveRequestFilesLinkExpirationInDays = if ($null -ne $ExpirationDays) { $ExpirationDays } else { $CurrentState.OneDriveRequestFilesLinkExpirationInDays } SharingCapability = if ($WantedState -eq $true) { 'External Users and Guests (Anyone)' } else { $SharingCapabilityEnum[$CurrentState.SharingCapability] } } Set-CIPPStandardsCompareField -FieldName 'standards.SPFileRequests' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant From 933a6dcdab4c5c29987482dbe5dee165ffad1922 Mon Sep 17 00:00:00 2001 From: Roel van der Wegen Date: Fri, 8 May 2026 19:54:26 +0200 Subject: [PATCH 33/36] Fix for 5979 --- .../Public/Standards/Invoke-CIPPStandardSPFileRequests.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPFileRequests.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPFileRequests.ps1 index 11cba32e784f..5219de031e03 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPFileRequests.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPFileRequests.ps1 @@ -157,8 +157,8 @@ function Invoke-CIPPStandardSPFileRequests { $CurrentValue = @{ CoreRequestFilesLinkEnabled = $CurrentState.CoreRequestFilesLinkEnabled OneDriveRequestFilesLinkEnabled = $CurrentState.OneDriveRequestFilesLinkEnabled - CoreRequestFilesLinkExpirationInDays = $CurrentState.CoreRequestFilesLinkExpirationInDays - OneDriveRequestFilesLinkExpirationInDays = $CurrentState.OneDriveRequestFilesLinkExpirationInDays + CoreRequestFilesLinkExpirationInDays = if ($null -ne $ExpirationDays -and $WantedState -eq $true) { $CurrentState.CoreRequestFilesLinkExpirationInDays } else { $null } + OneDriveRequestFilesLinkExpirationInDays = if ($null -ne $ExpirationDays -and $WantedState -eq $true) { $CurrentState.OneDriveRequestFilesLinkExpirationInDays } else { $null } SharingCapability = $SharingCapabilityEnum[$CurrentState.SharingCapability] } $ExpectedValue = @{ From 4a466be5c371ed280c758361e83d4676f3b82923 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 8 May 2026 14:57:25 -0400 Subject: [PATCH 34/36] fix: prevent stale template list from skewing applied standards report --- .../Standards/Invoke-ListStandardsCompare.ps1 | 34 ++++++++++++++++++ .../Invoke-listStandardTemplates.ps1 | 36 +++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListStandardsCompare.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListStandardsCompare.ps1 index 0b94eb63d919..b3f9b53f8c4c 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListStandardsCompare.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListStandardsCompare.ps1 @@ -13,6 +13,26 @@ function Invoke-ListStandardsCompare { $TenantFilter = $Request.Query.tenantFilter $TemplateFilter = $Request.Query.templateId + # Get-CIPPStandards is the authoritative source for what is currently in scope. + $StandardParams = @{} + if ($TemplateFilter) { $StandardParams.TemplateId = $TemplateFilter } + if ($TenantFilter) { $StandardParams.TenantFilter = $TenantFilter } + $StandardList = Get-CIPPStandards @StandardParams + + $ScopedTemplateGuids = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) + $ScopedQuarantineNames = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) + foreach ($Entry in $StandardList) { + switch ($Entry.Standard) { + { $_ -in @('IntuneTemplate', 'ConditionalAccessTemplate') } { + if ($Entry.Settings.TemplateList.value) { $null = $ScopedTemplateGuids.Add($Entry.Settings.TemplateList.value) } + } + 'QuarantineTemplate' { + $DisplayName = $Entry.Settings.displayName.value ?? $Entry.Settings.displayName + if ($DisplayName) { $null = $ScopedQuarantineNames.Add($DisplayName) } + } + } + } + $Filters = [system.collections.generic.list[string]]::new() if ($TenantFilter) { $Filters.Add("PartitionKey eq '{0}'" -f $TenantFilter) @@ -34,6 +54,20 @@ function Invoke-ListStandardsCompare { $FieldValue = $Standard.Value $Tenant = $Standard.PartitionKey + # Skip rows for template types no longer in scope per the current standard list. + if ($FieldName -match '^standards\.(IntuneTemplate|ConditionalAccessTemplate)\.(.+)$') { + if (-not $ScopedTemplateGuids.Contains($Matches[2])) { continue } + } elseif ($ScopedQuarantineNames.Count -gt 0 -and $FieldName -match '^standards\.QuarantineTemplate\.(.+)$') { + # Decode hex-encoded display name and check if it's still in scope + $HexEncoded = $Matches[1] + $Chars = [System.Collections.Generic.List[char]]::new() + for ($i = 0; $i -lt $HexEncoded.Length; $i += 2) { + $Chars.Add([char][Convert]::ToInt32($HexEncoded.Substring($i, 2), 16)) + } + $DecodedName = -join $Chars + if (-not $ScopedQuarantineNames.Contains($DecodedName)) { continue } + } + # decode field names that are hex encoded (e.g. QuarantineTemplates) if ($FieldName -match '^(standards\.QuarantineTemplate\.)(.+)$') { $Prefix = $Matches[1] diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-listStandardTemplates.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-listStandardTemplates.ps1 index 698fbb42deb3..de82fde161b3 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-listStandardTemplates.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-listStandardTemplates.ps1 @@ -35,6 +35,42 @@ function Invoke-listStandardTemplates { $Data.excludedTenants = @() } } + + # Re-expand TemplateList-Tags live so stale addedFields snapshots don't show removed templates + if ($Data.standards) { + foreach ($StandardName in $Data.standards.PSObject.Properties.Name) { + $StandardConfig = $Data.standards.$StandardName + $Items = if ($StandardConfig -is [System.Collections.IEnumerable] -and $StandardConfig -isnot [string]) { $StandardConfig } else { @($StandardConfig) } + foreach ($Item in $Items) { + if ($Item.'TemplateList-Tags' -and $Item.'TemplateList-Tags'.value) { + if (-not $IntuneTemplatesCache) { + $IntuneTable = Get-CippTable -tablename 'templates' + $IntuneFilter = "PartitionKey eq 'IntuneTemplate'" + $IntuneTemplatesCache = Get-CIPPAzDataTableEntity @IntuneTable -Filter $IntuneFilter + } + $PackageName = $Item.'TemplateList-Tags'.value + $LiveExpanded = @($IntuneTemplatesCache | Where-Object package -EQ $PackageName | ForEach-Object { + $TplJson = $_.JSON | ConvertFrom-Json -ErrorAction SilentlyContinue + [pscustomobject]@{ + GUID = $_.RowKey + displayName = if ($TplJson.displayName) { $TplJson.displayName } else { $_.RowKey } + name = if ($TplJson.displayName) { $TplJson.displayName } else { $_.RowKey } + } + }) + if ($Item.'TemplateList-Tags'.addedFields) { + $Item.'TemplateList-Tags'.addedFields | Add-Member -NotePropertyName 'templates' -NotePropertyValue $LiveExpanded -Force + } + if ($Item.'TemplateList-Tags'.rawData) { + $Item.'TemplateList-Tags'.rawData | Add-Member -NotePropertyName 'templates' -NotePropertyValue $LiveExpanded -Force + } + if (-not $Item.'TemplateList-Tags'.addedFields -and -not $Item.'TemplateList-Tags'.rawData) { + $Item.'TemplateList-Tags' | Add-Member -NotePropertyName 'addedFields' -NotePropertyValue ([pscustomobject]@{ templates = $LiveExpanded }) -Force + } + } + } + } + } + $Data } } | Sort-Object -Property templateName From bf8a33a6dae91aa0aaa3e880b70ae0acedd078a5 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 8 May 2026 17:09:55 -0400 Subject: [PATCH 35/36] feat: add Invoke-ListResellerRelationshipLink function for retrieving reseller relationship links implements https://github.com/KelvinTegelaar/CIPP/issues/5963 --- .../Invoke-ListResellerRelationshipLink.ps1 | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListResellerRelationshipLink.ps1 diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListResellerRelationshipLink.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListResellerRelationshipLink.ps1 new file mode 100644 index 000000000000..1d48e1a8591e --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListResellerRelationshipLink.ps1 @@ -0,0 +1,37 @@ +function Invoke-ListResellerRelationshipLink { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + Tenant.Relationship.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $StatusCode = [HttpStatusCode]::OK + $Body = @{} + + # Get the indirect reseller relationship invite link + try { + $RelationshipRequest = New-GraphGetRequest -uri 'https://api.partnercenter.microsoft.com/v1/customers/relationshiprequests?dualRoleIndirectRelationship=false' -scope 'https://api.partnercenter.microsoft.com/.default' -NoAuthCheck $true + $Body.inviteUrl = $RelationshipRequest.url + } catch { + $Body.inviteUrl = $null + $Body.inviteUrlError = "Failed to retrieve relationship invite link: $($_.Exception.Message)" + Write-Information "ListResellerRelationshipLink: Failed to get invite URL - $($_.Exception.Message)" + } + + # Get indirect providers (for Tier 2 / indirect resellers) + try { + $RelationshipsResponse = New-GraphGetRequest -uri 'https://api.partnercenter.microsoft.com/v1/relationships?relationship_type=IsIndirectResellerOf' -scope 'https://api.partnercenter.microsoft.com/.default' -NoAuthCheck $true + $Body.indirectProviders = @($RelationshipsResponse.items) + } catch { + $Body.indirectProviders = @() + Write-Information "ListResellerRelationshipLink: Failed to get indirect providers - $($_.Exception.Message)" + } + + return [HttpResponseContext]@{ + StatusCode = $StatusCode + Body = $Body + } +} From e99b4aaabd288134f1ea5fed81ad071dc10d19c5 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 8 May 2026 19:17:20 -0400 Subject: [PATCH 36/36] chore: bump version to 10.4.4 --- host.json | 2 +- version_latest.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/host.json b/host.json index 804b0ab6ca5e..473ab1bfce11 100644 --- a/host.json +++ b/host.json @@ -16,7 +16,7 @@ "distributedTracingEnabled": false, "version": "None" }, - "defaultVersion": "10.4.3", + "defaultVersion": "10.4.4", "versionMatchStrategy": "Strict", "versionFailureStrategy": "Fail" } diff --git a/version_latest.txt b/version_latest.txt index 44c1a8619b2b..622a6f75b792 100644 --- a/version_latest.txt +++ b/version_latest.txt @@ -1 +1 @@ -10.4.3 +10.4.4