From ee0398db8fb1dc0f3e068d57044486d90b03c992 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Thu, 14 May 2026 11:28:32 +0200 Subject: [PATCH 001/202] feat(endpoint): add Apple ADE and Android enrollment profile listing and deletion endpoints --- .../Invoke-ExecRemoveEnrollmentProfile.ps1 | 46 ++++++++++ .../Invoke-ListAndroidEnrollmentProfiles.ps1 | 55 ++++++++++++ .../Invoke-ListAppleEnrollmentProfiles.ps1 | 83 +++++++++++++++++++ cspell.json | 2 + 4 files changed, 186 insertions(+) create mode 100644 Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ExecRemoveEnrollmentProfile.ps1 create mode 100644 Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListAndroidEnrollmentProfiles.ps1 create mode 100644 Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListAppleEnrollmentProfiles.ps1 diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ExecRemoveEnrollmentProfile.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ExecRemoveEnrollmentProfile.ps1 new file mode 100644 index 000000000000..2d1fa7e8521c --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ExecRemoveEnrollmentProfile.ps1 @@ -0,0 +1,46 @@ +function Invoke-ExecRemoveEnrollmentProfile { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Endpoint.MEM.ReadWrite + .DESCRIPTION + Deletes an Apple ADE or Android Enterprise enrollment profile. + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + $TenantFilter = $Request.Body.tenantFilter + $ProfileId = $Request.Body.profileId ?? $Request.Body.id + $ProfileType = $Request.Body.profileType ?? 'apple' + $TokenId = $Request.Body.tokenId + $DisplayName = $Request.Body.displayName ?? $ProfileId + + try { + if ([string]::IsNullOrWhiteSpace($ProfileId)) { throw 'No profile id was supplied.' } + + if ($ProfileType -eq 'android') { + $Uri = "https://graph.microsoft.com/beta/deviceManagement/androidDeviceOwnerEnrollmentProfiles/$ProfileId" + } else { + if ([string]::IsNullOrWhiteSpace($TokenId)) { throw 'No Apple ADE token id was supplied.' } + $Uri = "https://graph.microsoft.com/beta/deviceManagement/depOnboardingSettings/$TokenId/enrollmentProfiles/$ProfileId" + } + + $null = New-GraphPOSTRequest -uri $Uri -tenantid $TenantFilter -type DELETE + $Result = "Deleted enrollment profile $DisplayName" + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Info + $StatusCode = [HttpStatusCode]::OK + } catch { + $StatusCode = [HttpStatusCode]::InternalServerError + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to delete enrollment profile ${DisplayName}: $($ErrorMessage.NormalizedMessage)" + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Error -LogData $ErrorMessage + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{ Results = $Result } + }) +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListAndroidEnrollmentProfiles.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListAndroidEnrollmentProfiles.ps1 new file mode 100644 index 000000000000..3cd4218a50fc --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListAndroidEnrollmentProfiles.ps1 @@ -0,0 +1,55 @@ +function Invoke-ListAndroidEnrollmentProfiles { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Endpoint.MEM.Read + .DESCRIPTION + Lists Android Enterprise enrollment profiles and hydrates token fields when Graph omits them from the list response. + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter + $Select = 'id,displayName,description,enrollmentMode,enrollmentTokenType,enrolledDeviceCount,tokenExpirationDateTime,lastModifiedDateTime,tokenValue,qrCodeContent,qrCodeImage' + $EncodedSelect = [System.Uri]::EscapeDataString($Select) + $BaseUri = 'https://graph.microsoft.com/beta/deviceManagement/androidDeviceOwnerEnrollmentProfiles' + + try { + $EnrollmentProfiles = @(New-GraphGetRequest -uri "${BaseUri}?`$select=$EncodedSelect" -tenantid $TenantFilter) + $Results = foreach ($EnrollmentProfile in $EnrollmentProfiles) { + $ProfileObject = $EnrollmentProfile | Select-Object * + $MissingTokenData = [string]::IsNullOrWhiteSpace($ProfileObject.tokenValue) -and + [string]::IsNullOrWhiteSpace($ProfileObject.qrCodeContent) -and + [string]::IsNullOrWhiteSpace($ProfileObject.qrCodeImage.value) + + if (($ProfileObject.enrollmentMode -eq 'corporateOwnedAOSPUserlessDevice' -or $ProfileObject.enrollmentMode -eq 'corporateOwnedAOSPUserAssociatedDevice') -and $MissingTokenData -and -not [string]::IsNullOrWhiteSpace($ProfileObject.id)) { + try { + $ProfileDetails = New-GraphGetRequest -uri "$BaseUri/$($ProfileObject.id)?`$select=$EncodedSelect" -tenantid $TenantFilter + foreach ($Property in $ProfileDetails.PSObject.Properties) { + $ProfileObject | Add-Member -NotePropertyName $Property.Name -NotePropertyValue $Property.Value -Force + } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to hydrate Android enrollment token fields for profile $($ProfileObject.displayName ?? $ProfileObject.id)" -Sev Warning -LogData $ErrorMessage + } + } + + $ProfileObject + } + + $StatusCode = [HttpStatusCode]::OK + } catch { + $StatusCode = [HttpStatusCode]::InternalServerError + $ErrorMessage = Get-CippException -Exception $_ + $Results = "Failed to list Android enrollment profiles: $($ErrorMessage.NormalizedMessage)" + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message $Results -Sev Error -LogData $ErrorMessage + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{ Results = @($Results) } + }) +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListAppleEnrollmentProfiles.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListAppleEnrollmentProfiles.ps1 new file mode 100644 index 000000000000..feb5afc8d9d8 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListAppleEnrollmentProfiles.ps1 @@ -0,0 +1,83 @@ +function Invoke-ListAppleEnrollmentProfiles { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Endpoint.MEM.Read + .DESCRIPTION + Lists Apple Automated Device Enrollment tokens and enrollment profiles. + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter + + try { + $DepOnboardingSettings = @(New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/depOnboardingSettings' -tenantid $TenantFilter) + $Tokens = foreach ($DepSetting in $DepOnboardingSettings) { + $Token = $DepSetting | Select-Object * + $Token | Add-Member -NotePropertyName 'daysUntilExpiration' -NotePropertyValue $( + if ($Token.tokenExpirationDateTime) { + [math]::Floor(([datetime]$Token.tokenExpirationDateTime - [datetime]::UtcNow).TotalDays) + } else { + $null + } + ) -Force + $Token | Add-Member -NotePropertyName 'isExpired' -NotePropertyValue $( + if ($Token.tokenExpirationDateTime) { ([datetime]$Token.tokenExpirationDateTime) -lt [datetime]::UtcNow } else { $false } + ) -Force + $Token + } + + $Profiles = foreach ($DepSetting in $DepOnboardingSettings) { + if ([string]::IsNullOrWhiteSpace($DepSetting.id)) { continue } + + try { + $EnrollmentProfiles = @(New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/depOnboardingSettings/$($DepSetting.id)/enrollmentProfiles" -tenantid $TenantFilter) + foreach ($EnrollmentProfile in $EnrollmentProfiles) { + $ProfileType = $EnrollmentProfile.'@odata.type' + $Platform = switch -Regex ($ProfileType) { + 'depMacOSEnrollmentProfile' { 'macOS'; break } + 'depIOSEnrollmentProfile' { 'iOS/iPadOS'; break } + 'depVisionOSEnrollmentProfile' { 'visionOS'; break } + 'depTvOSEnrollmentProfile' { 'tvOS'; break } + default { 'Unknown' } + } + + $ProfileObject = $EnrollmentProfile | Select-Object * + $ProfileObject | Add-Member -NotePropertyName 'platform' -NotePropertyValue $Platform -Force + $ProfileObject | Add-Member -NotePropertyName 'profileType' -NotePropertyValue 'apple' -Force + $ProfileObject | Add-Member -NotePropertyName 'tokenId' -NotePropertyValue $DepSetting.id -Force + $ProfileObject | Add-Member -NotePropertyName 'tokenName' -NotePropertyValue $DepSetting.tokenName -Force + $ProfileObject | Add-Member -NotePropertyName 'appleIdentifier' -NotePropertyValue $DepSetting.appleIdentifier -Force + $ProfileObject | Add-Member -NotePropertyName 'tokenExpirationDateTime' -NotePropertyValue $DepSetting.tokenExpirationDateTime -Force + $ProfileObject | Add-Member -NotePropertyName 'tokenType' -NotePropertyValue $DepSetting.tokenType -Force + $ProfileObject + } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to list Apple ADE profiles for token $($DepSetting.tokenName)" -Sev Warning -LogData $ErrorMessage + } + } + + $StatusCode = [HttpStatusCode]::OK + $Body = @{ + Results = @{ + Tokens = @($Tokens) + Profiles = @($Profiles) + } + } + } catch { + $StatusCode = [HttpStatusCode]::InternalServerError + $ErrorMessage = Get-CippException -Exception $_ + $Body = @{ Results = "Failed to list Apple ADE enrollment profiles: $($ErrorMessage.NormalizedMessage)" } + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message $Body.Results -Sev Error -LogData $ErrorMessage + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = $Body + }) +} diff --git a/cspell.json b/cspell.json index 16d358d26a90..42bdb4bb4fa4 100644 --- a/cspell.json +++ b/cspell.json @@ -7,6 +7,7 @@ "adminapi", "ADMS", "AITM", + "AOSP", "Autotask", "Bluetrait", "cipp", @@ -50,6 +51,7 @@ "Standardcal", "Terrl", "TNEF", + "Userless", "weburl", "winmail", "Yubikey" From 5ccf15a9197c67cb8fdde40606dd7b26a476ae1a Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Thu, 14 May 2026 20:23:40 +0200 Subject: [PATCH 002/202] fix: missing odata path error in the returned json Aka fix my own mistake lol --- .../Endpoint/MEM/Invoke-ListIntunePolicy.ps1 | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) 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 c01f718c2a5f..8461ef571cdf 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 @@ -78,8 +78,14 @@ function Invoke-ListIntunePolicy { } if ($DefinitionRequests.Count -gt 0) { + $HasDefinitionFailures = $false $DefinitionResults = New-GraphBulkRequest -Requests @($DefinitionRequests) -tenantid $TenantFilter foreach ($DefinitionResult in $DefinitionResults) { + if ($DefinitionResult.status -ne 200) { + $HasDefinitionFailures = $true + continue + } + $SettingId = $SettingIdMap[$DefinitionResult.id] $Setting = $GraphRequest.settings | Where-Object { $_.id -eq $SettingId } | Select-Object -First 1 if ($Setting) { @@ -87,6 +93,34 @@ function Invoke-ListIntunePolicy { $Setting | Add-Member -NotePropertyName settingDefinitions -NotePropertyValue $Definitions -Force } } + + if ($HasDefinitionFailures -and $GraphRequest.templateReference.templateId) { + try { + $TemplateSettingsResponse = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/configurationPolicyTemplates('$($GraphRequest.templateReference.templateId)')/settingTemplates?`$expand=settingDefinitions&`$top=1000" -tenantid $TenantFilter + $TemplateSettings = @($TemplateSettingsResponse.value ?? $TemplateSettingsResponse) + $TemplateDefinitionsByInstanceId = @{} + + foreach ($TemplateSetting in $TemplateSettings) { + $TemplateInstanceId = $TemplateSetting.settingInstanceTemplate.settingInstanceTemplateId + $TemplateDefinitions = @($TemplateSetting.settingDefinitions | Where-Object { $_.id }) + if ($TemplateInstanceId -and $TemplateDefinitions.Count -gt 0) { + $TemplateDefinitionsByInstanceId[$TemplateInstanceId] = $TemplateDefinitions + } + } + + foreach ($Setting in $GraphRequest.settings) { + $ExistingDefinitions = @($Setting.settingDefinitions | Where-Object { $_.id }) + if ($ExistingDefinitions.Count -gt 0) { continue } + + $TemplateInstanceId = $Setting.settingInstance.settingInstanceTemplateReference.settingInstanceTemplateId + if ($TemplateInstanceId -and $TemplateDefinitionsByInstanceId.ContainsKey($TemplateInstanceId)) { + $Setting | Add-Member -NotePropertyName settingDefinitions -NotePropertyValue $TemplateDefinitionsByInstanceId[$TemplateInstanceId] -Force + } + } + } catch { + Write-Information "Could not retrieve configuration policy template definitions for ${ID}: $($_.Exception.Message)" + } + } } } } elseif ($URLName -ieq 'GroupPolicyConfigurations') { From 5289a3092d34df70035ab12a2cc70daabeb2e77f Mon Sep 17 00:00:00 2001 From: Integrated Solutions Date: Fri, 15 May 2026 15:57:00 +1000 Subject: [PATCH 003/202] feat: ability to add/remove nested groups in group memberships --- .../HTTP Functions/Invoke-ListUsersAndGroups.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListUsersAndGroups.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListUsersAndGroups.ps1 index 5abd101abc09..4064ec8345bd 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListUsersAndGroups.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListUsersAndGroups.ps1 @@ -22,12 +22,12 @@ function Invoke-ListUsersAndGroups { @{ id = 'groups' method = 'GET' - url = "groups?`$select=id,displayName&`$top=999" + url = "groups?`$select=id,displayName,groupTypes,mailEnabled,securityEnabled&`$top=999" } ) $BulkResults = New-GraphBulkRequest -Requests $BulkRequests -tenantid $TenantFilter $Users = ($BulkResults | Where-Object { $_.id -eq 'users' }).body.value | Select-Object *, @{Name = '@odata.type'; Expression = { '#microsoft.graph.user' } } - $Groups = ($BulkResults | Where-Object { $_.id -eq 'groups' }).body.value | Select-Object id, displayName, @{Name = 'userPrincipalName'; Expression = { $null } }, @{Name = '@odata.type'; Expression = { '#microsoft.graph.group' } } + $Groups = ($BulkResults | Where-Object { $_.id -eq 'groups' }).body.value | Where-Object { $_.groupTypes -notcontains 'Unified' } | Select-Object id, displayName, mailEnabled, securityEnabled, @{Name = 'userPrincipalName'; Expression = { $null } }, @{Name = '@odata.type'; Expression = { '#microsoft.graph.group' } } $GraphRequest = @($Users) + @($Groups) | Sort-Object displayName $StatusCode = [HttpStatusCode]::OK } catch { @@ -37,6 +37,6 @@ function Invoke-ListUsersAndGroups { } return [HttpResponseContext]@{ StatusCode = $StatusCode - Body = @($GraphRequest) + Body = @{ Results = @($GraphRequest) } } } From 7ae35c2bd5c8f11169722651e69c18224dc8fde8 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Fri, 15 May 2026 02:28:44 -0500 Subject: [PATCH 004/202] post exec tweaks for dedupe queue names --- Config/FeatureFlags.json | 6 +- .../Push-CIPPDBCacheApplyBatch.ps1 | 3 +- .../Tests/Push-CIPPTestsApplyBatch.ps1 | 3 +- .../Start-CIPPDBTestsRun.ps1 | 6 +- .../CIPP/Settings/Invoke-ListWorkerHealth.ps1 | 110 ++++++++++++++++++ .../HTTP Functions/Invoke-ExecTestRun.ps1 | 3 +- 6 files changed, 124 insertions(+), 7 deletions(-) create mode 100644 Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListWorkerHealth.ps1 diff --git a/Config/FeatureFlags.json b/Config/FeatureFlags.json index 618c8c242094..35c51a44e865 100644 --- a/Config/FeatureFlags.json +++ b/Config/FeatureFlags.json @@ -34,13 +34,15 @@ "ListCIPPUsers", "ExecSSOSetup", "ExecContainerManagement", - "ListContainerLogs" + "ListContainerLogs", + "ListWorkerHealth" ], "Pages": [ "/cipp/advanced/super-admin/cipp-users", "/cipp/advanced/super-admin/sso", "/cipp/advanced/super-admin/container", - "/cipp/advanced/container-logs" + "/cipp/advanced/container-logs", + "/cipp/advanced/worker-health" ], "Hidden": true } diff --git a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheApplyBatch.ps1 b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheApplyBatch.ps1 index 52b60b72436a..5a277a22d886 100644 --- a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheApplyBatch.ps1 +++ b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheApplyBatch.ps1 @@ -35,8 +35,9 @@ function Push-CIPPDBCacheApplyBatch { Write-Information "Aggregated $($AllTasks.Count) cache tasks from all tenants" # Start a single flat orchestrator to execute all cache tasks + $TenantSuffix = if ($Item.Parameters.TenantFilter) { "_$($Item.Parameters.TenantFilter)" } else { '' } $InputObject = [PSCustomObject]@{ - OrchestratorName = 'CIPPDBCacheExecute' + OrchestratorName = "CIPPDBCacheExecute$TenantSuffix" Batch = @($AllTasks) SkipLog = $true } diff --git a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Tests/Push-CIPPTestsApplyBatch.ps1 b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Tests/Push-CIPPTestsApplyBatch.ps1 index aa12830a0a06..41236b25c316 100644 --- a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Tests/Push-CIPPTestsApplyBatch.ps1 +++ b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Tests/Push-CIPPTestsApplyBatch.ps1 @@ -35,8 +35,9 @@ function Push-CIPPTestsApplyBatch { Write-Information "Aggregated $($AllTasks.Count) test tasks from all tenants" # Start a single flat orchestrator to execute all test tasks + $TenantSuffix = if ($Item.Parameters.TenantFilter) { "_$($Item.Parameters.TenantFilter)" } else { '' } $InputObject = [PSCustomObject]@{ - OrchestratorName = 'CIPPTestsExecute' + OrchestratorName = "CIPPTestsExecute$TenantSuffix" Batch = @($AllTasks) SkipLog = $true } diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPDBTestsRun.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPDBTestsRun.ps1 index abf3057fb386..f310233164ba 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPDBTestsRun.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPDBTestsRun.ps1 @@ -69,12 +69,16 @@ function Start-CIPPDBTestsRun { Write-Information "Built batch of $($Batch.Count) tenant test list activities" # Phase 2 via PostExecution: Aggregate all task lists and start flat execution orchestrator + $NameSuffix = if ($TenantFilter -ne 'allTenants') { "-$TenantFilter" } else { '' } $InputObject = [PSCustomObject]@{ - OrchestratorName = 'TestsList' + OrchestratorName = "TestsList$NameSuffix" Batch = @($Batch) SkipLog = $true PostExecution = @{ FunctionName = 'CIPPTestsApplyBatch' + Parameters = @{ + TenantFilter = $TenantFilter + } } } diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListWorkerHealth.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListWorkerHealth.ps1 new file mode 100644 index 000000000000..66862547597b --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListWorkerHealth.ps1 @@ -0,0 +1,110 @@ +function Invoke-ListWorkerHealth { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + CIPP.SuperAdmin.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Action = $Request.Query.Action ?? 'Snapshot' + + try { + switch ($Action) { + 'Snapshot' { + $Snapshot = [Craft.Services.WorkerMetricsBridge]::GetSnapshot() + $Body = @{ Results = $Snapshot } + } + 'Summary' { + $Summary = [Craft.Services.WorkerMetricsBridge]::GetSummary() + $Body = @{ Results = $Summary } + } + 'Pool' { + $PoolType = $Request.Query.PoolType ?? 'http' + $Pool = [Craft.Services.WorkerMetricsBridge]::GetPoolMetrics($PoolType) + $Body = @{ Results = $Pool } + } + 'Jobs' { + $RunName = $Request.Query.RunName + $Status = $Request.Query.Status + $Limit = if ($Request.Query.Limit) { [int]$Request.Query.Limit } else { 100 } + $Jobs = [Craft.Services.WorkerMetricsBridge]::GetJobDetails($RunName, $Status, $Limit) + $Body = @{ Results = $Jobs } + } + 'Runs' { + $Runs = [Craft.Services.WorkerMetricsBridge]::GetRunSummaries() + $Body = @{ Results = $Runs } + } + 'CancelJob' { + $JobId = $Request.Query.JobId ?? $Request.Body.JobId + if (-not $JobId) { + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = @{ Results = 'JobId is required' } + } + } + $Result = [Craft.Services.WorkerMetricsBridge]::CancelJob($JobId) + $Body = @{ Results = @{ Success = $Result; JobId = $JobId } } + } + 'CancelRun' { + $RunName = $Request.Query.RunName ?? $Request.Body.RunName + if (-not $RunName) { + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = @{ Results = 'RunName is required' } + } + } + $Cancelled = [Craft.Services.WorkerMetricsBridge]::CancelRun($RunName) + $Body = @{ Results = @{ Success = $true; RunName = $RunName; CancelledCount = $Cancelled } } + } + 'DeleteJob' { + $JobId = $Request.Query.JobId ?? $Request.Body.JobId + if (-not $JobId) { + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = @{ Results = 'JobId is required' } + } + } + $Result = [Craft.Services.WorkerMetricsBridge]::DeleteJob($JobId) + $Body = @{ Results = @{ Success = $Result; JobId = $JobId } } + } + 'PurgeCompleted' { + $Purged = [Craft.Services.WorkerMetricsBridge]::PurgeCompleted() + $Body = @{ Results = @{ Success = $true; PurgedCount = $Purged } } + } + 'ChangePriority' { + $JobId = $Request.Query.JobId ?? $Request.Body.JobId + $NewPriority = $Request.Query.Priority ?? $Request.Body.Priority + if (-not $JobId -or $null -eq $NewPriority) { + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = @{ Results = 'JobId and Priority are required' } + } + } + $Result = [Craft.Services.WorkerMetricsBridge]::ChangePriority($JobId, [int]$NewPriority) + $Body = @{ Results = @{ Success = $Result; JobId = $JobId; NewPriority = [int]$NewPriority } } + } + default { + $Body = @{ Results = "Unknown action: $Action" } + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = $Body + } + } + } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API $APIName -message "Worker health error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::InternalServerError + Body = @{ Results = "Failed: $($ErrorMessage.NormalizedError)" } + } + } + + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = $Body + } +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ExecTestRun.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ExecTestRun.ps1 index bd7fe721f34e..47eac1c03f7d 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ExecTestRun.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ExecTestRun.ps1 @@ -17,12 +17,11 @@ function Invoke-ExecTestRun { @{ FunctionName = 'CIPPDBCacheData' TenantFilter = $TenantFilter - QueueId = $Queue.RowKey QueueName = "Cache - $TenantFilter" } ) $InputObject = [PSCustomObject]@{ - OrchestratorName = 'TestDataCollectionAndRun' + OrchestratorName = "TestDataCollectionAndRun-$TenantFilter" Batch = $Batch SkipLog = $false PostExecution = @{ From fd6e30f62fb209f2b706b359bd1b91f5ef368081 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Fri, 15 May 2026 20:29:45 +0200 Subject: [PATCH 005/202] fix(standards): target azureADRegistration in intuneRestrictUserDeviceRegistration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The standard was writing to azureADJoin.allowedToJoin despite its name — that's the join scope, not registration. Re-point reads and writes to azureADRegistration.allowedToRegister. Co-Authored-By: Claude Opus 4.7 --- ...rdintuneRestrictUserDeviceRegistration.ps1 | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardintuneRestrictUserDeviceRegistration.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardintuneRestrictUserDeviceRegistration.ps1 index bf9b6263633e..345ee1092b1b 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardintuneRestrictUserDeviceRegistration.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardintuneRestrictUserDeviceRegistration.ps1 @@ -40,7 +40,8 @@ function Invoke-CIPPStandardintuneRestrictUserDeviceRegistration { return } # Current M365 Config - $CurrentOdataType = $PreviousSetting.azureADJoin.allowedToJoin.'@odata.type' + $CurrentOdataType = $PreviousSetting.azureADRegistration.allowedToRegister.'@odata.type' + $IsAdminConfigurable = [bool]$PreviousSetting.azureADRegistration.isAdminConfigurable # Standards Config $DisableUserDeviceRegistration = [bool]$Settings.disableUserDeviceRegistration @@ -53,29 +54,31 @@ function Invoke-CIPPStandardintuneRestrictUserDeviceRegistration { if ($Settings.remediate -eq $true) { if ($StateIsCorrect -eq $true) { - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Device registration restriction is already configured (registering users allowed to join: $DesiredStateText)." -sev Info + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Device registration restriction is already configured (users allowed to register: $DesiredStateText)." -sev Info + } elseif ($IsAdminConfigurable -eq $false) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Cannot remediate device registration restriction: azureADRegistration.isAdminConfigurable is false for this tenant (commonly because Intune is enabled). Skipping remediation.' -sev Warn } else { try { - $PreviousSetting.azureADJoin.allowedToJoin = @{ '@odata.type' = $DesiredOdataType; users = $null; groups = $null } + $PreviousSetting.azureADRegistration.allowedToRegister = @{ '@odata.type' = $DesiredOdataType; users = $null; groups = $null } $NewBody = ConvertTo-Json -Compress -InputObject $PreviousSetting -Depth 10 New-GraphPostRequest -tenantid $Tenant -Uri 'https://graph.microsoft.com/beta/policies/deviceRegistrationPolicy' -Type PUT -Body $NewBody -ContentType 'application/json' $CurrentOdataType = $DesiredOdataType $CurrentDisableUserDeviceRegistration = ($CurrentOdataType -eq '#microsoft.graph.noDeviceRegistrationMembership') $StateIsCorrect = ($CurrentOdataType -eq $DesiredOdataType) - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Set device registration restriction (registering users allowed to join: $DesiredStateText)." -sev Info + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Set device registration restriction (users allowed to register: $DesiredStateText)." -sev Info } catch { $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to set device registration restriction (registering users allowed to join: $DesiredStateText). Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to set device registration restriction (users allowed to register: $DesiredStateText). Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage } } } if ($Settings.alert -eq $true) { if ($StateIsCorrect -eq $true) { - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Device registration restriction is configured as expected (registering users allowed to join: $DesiredStateText)." -sev Info + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Device registration restriction is configured as expected (users allowed to register: $DesiredStateText)." -sev Info } else { - Write-StandardsAlert -message "Device registration restriction is not configured as expected (registering users allowed to join: $DesiredStateText)" -object @{ current = @{ disableUserDeviceRegistration = $CurrentDisableUserDeviceRegistration }; desired = @{ disableUserDeviceRegistration = $DisableUserDeviceRegistration } } -tenant $Tenant -standardName 'intuneRestrictUserDeviceRegistration' -standardId $Settings.standardId - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Device registration restriction is not configured as expected (registering users allowed to join: $DesiredStateText)." -sev Info + Write-StandardsAlert -message "Device registration restriction is not configured as expected (users allowed to register: $DesiredStateText)" -object @{ current = @{ disableUserDeviceRegistration = $CurrentDisableUserDeviceRegistration }; desired = @{ disableUserDeviceRegistration = $DisableUserDeviceRegistration } } -tenant $Tenant -standardName 'intuneRestrictUserDeviceRegistration' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Device registration restriction is not configured as expected (users allowed to register: $DesiredStateText)." -sev Info } } From c67bc8dd9b892ebfaeaf80a9037941d46d53d745 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Fri, 15 May 2026 20:29:46 +0200 Subject: [PATCH 006/202] feat(standards): add intuneRestrictUserDeviceJoin standard Restores the azureADJoin.allowedToJoin behavior that previously lived (mislabeled) inside intuneRestrictUserDeviceRegistration. Join and registration are independent scopes, so each needs its own standard. Co-Authored-By: Claude Opus 4.7 --- ...PPStandardintuneRestrictUserDeviceJoin.ps1 | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardintuneRestrictUserDeviceJoin.ps1 diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardintuneRestrictUserDeviceJoin.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardintuneRestrictUserDeviceJoin.ps1 new file mode 100644 index 000000000000..e036f2577e91 --- /dev/null +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardintuneRestrictUserDeviceJoin.ps1 @@ -0,0 +1,95 @@ +function Invoke-CIPPStandardintuneRestrictUserDeviceJoin { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) intuneRestrictUserDeviceJoin + .SYNOPSIS + (Label) Configure user restriction for Entra device join + .DESCRIPTION + (Helptext) Controls whether users can join devices to Entra. + (DocsDescription) Configures whether users can join devices to Entra. When disabled, users are unable to Entra-join devices, which prevents them from creating new Entra-joined (cloud-managed) device identities. + .NOTES + CAT + Entra (AAD) Standards + TAG + EXECUTIVETEXT + Controls whether employees can join their devices to the corporate Entra directory. Disabling user device join prevents unauthorized or unmanaged devices from becoming corporate-managed identities, enhancing overall security posture. + ADDEDCOMPONENT + {"type":"switch","name":"standards.intuneRestrictUserDeviceJoin.disableUserDeviceJoin","label":"Disable users from joining devices","defaultValue":true} + IMPACT + High Impact + ADDEDDATE + 2026-05-15 + POWERSHELLEQUIVALENT + Update-MgBetaPolicyDeviceRegistrationPolicy + RECOMMENDEDBY + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + .LINK + https://docs.cipp.app/user-documentation/tenant/standards/alignment/templates/available-standards + #> + + param($Tenant, $Settings) + + try { + $PreviousSetting = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/deviceRegistrationPolicy' -tenantid $Tenant + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the intuneRestrictUserDeviceJoin state for $Tenant. Error: $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage + return + } + # Current M365 Config + $CurrentOdataType = $PreviousSetting.azureADJoin.allowedToJoin.'@odata.type' + $IsAdminConfigurable = [bool]$PreviousSetting.azureADJoin.isAdminConfigurable + + # Standards Config + $DisableUserDeviceJoin = [bool]$Settings.disableUserDeviceJoin + + # State comparison + $DesiredOdataType = if ($DisableUserDeviceJoin) { '#microsoft.graph.noDeviceRegistrationMembership' } else { '#microsoft.graph.allDeviceRegistrationMembership' } + $CurrentDisableUserDeviceJoin = ($CurrentOdataType -eq '#microsoft.graph.noDeviceRegistrationMembership') + $StateIsCorrect = ($CurrentOdataType -eq $DesiredOdataType) + $DesiredStateText = if ($DisableUserDeviceJoin) { 'disabled' } else { 'enabled' } + + if ($Settings.remediate -eq $true) { + if ($StateIsCorrect -eq $true) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Device join restriction is already configured (users allowed to join: $DesiredStateText)." -sev Info + } elseif ($IsAdminConfigurable -eq $false) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Cannot remediate device join restriction: azureADJoin.isAdminConfigurable is false for this tenant. Skipping remediation.' -sev Warn + } else { + try { + $PreviousSetting.azureADJoin.allowedToJoin = @{ '@odata.type' = $DesiredOdataType; users = $null; groups = $null } + $NewBody = ConvertTo-Json -Compress -InputObject $PreviousSetting -Depth 10 + New-GraphPostRequest -tenantid $Tenant -Uri 'https://graph.microsoft.com/beta/policies/deviceRegistrationPolicy' -Type PUT -Body $NewBody -ContentType 'application/json' + $CurrentOdataType = $DesiredOdataType + $CurrentDisableUserDeviceJoin = ($CurrentOdataType -eq '#microsoft.graph.noDeviceRegistrationMembership') + $StateIsCorrect = ($CurrentOdataType -eq $DesiredOdataType) + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Set device join restriction (users allowed to join: $DesiredStateText)." -sev Info + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to set device join restriction (users allowed to join: $DesiredStateText). Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + } + } + } + + if ($Settings.alert -eq $true) { + if ($StateIsCorrect -eq $true) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Device join restriction is configured as expected (users allowed to join: $DesiredStateText)." -sev Info + } else { + Write-StandardsAlert -message "Device join restriction is not configured as expected (users allowed to join: $DesiredStateText)" -object @{ current = @{ disableUserDeviceJoin = $CurrentDisableUserDeviceJoin }; desired = @{ disableUserDeviceJoin = $DisableUserDeviceJoin } } -tenant $Tenant -standardName 'intuneRestrictUserDeviceJoin' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Device join restriction is not configured as expected (users allowed to join: $DesiredStateText)." -sev Info + } + } + + if ($Settings.report -eq $true) { + $CurrentValue = @{ + disableUserDeviceJoin = $CurrentDisableUserDeviceJoin + } + $ExpectedValue = @{ + disableUserDeviceJoin = $DisableUserDeviceJoin + } + Set-CIPPStandardsCompareField -FieldName 'standards.intuneRestrictUserDeviceJoin' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant + Add-CIPPBPAField -FieldName 'intuneRestrictUserDeviceJoin' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant + } +} From 90b6457d590ea6f6b96c62e8e0315ccd1f56f640 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Mon, 18 May 2026 07:16:57 -0400 Subject: [PATCH 007/202] Add AutoExpandingArchiveScope property showing org-level vs mailbox-level enablement --- .../Users/Invoke-ListUserMailboxDetails.ps1 | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserMailboxDetails.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserMailboxDetails.ps1 index 2cc8b8d3978e..01d95a67a2d5 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserMailboxDetails.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserMailboxDetails.ps1 @@ -83,11 +83,16 @@ function Invoke-ListUserMailboxDetails { $ArchiveEnabled = $false } - # Get organization config of auto-expanding archive if it's disabled on user level - if (-not $MailboxDetailedRequest.AutoExpandingArchiveEnabled -and $ArchiveEnabled) { - $AutoExpandingArchiveEnabled = $OrgConfig.AutoExpandingArchiveEnabled + # Check org-level first; if enabled org-wide, report that. Otherwise use mailbox-specific value. + if ($OrgConfig.AutoExpandingArchiveEnabled) { + $AutoExpandingArchiveEnabled = $true + $AutoExpandingArchiveScope = 'Organization' + } elseif ($MailboxDetailedRequest.AutoExpandingArchiveEnabled) { + $AutoExpandingArchiveEnabled = $true + $AutoExpandingArchiveScope = 'Mailbox' } else { - $AutoExpandingArchiveEnabled = $MailboxDetailedRequest.AutoExpandingArchiveEnabled + $AutoExpandingArchiveEnabled = $false + $AutoExpandingArchiveScope = 'None' } } catch { $ArchiveEnabled = $false @@ -260,6 +265,7 @@ function Invoke-ListUserMailboxDetails { BlockedForSpam = $BlockedForSpam ArchiveMailBox = $ArchiveEnabled AutoExpandingArchive = $AutoExpandingArchiveEnabled + AutoExpandingArchiveScope = $AutoExpandingArchiveScope RecipientTypeDetails = $MailboxDetailedRequest.RecipientTypeDetails Mailbox = $MailboxDetailedRequest RetentionPolicy = $MailboxDetailedRequest.RetentionPolicy From ab83a2bb5dbcfba32770827466b17a882398898d Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Mon, 18 May 2026 07:56:08 -0400 Subject: [PATCH 008/202] Update Update-CIPPSAMRedirectUri.ps1 --- .../Public/Authentication/Update-CIPPSAMRedirectUri.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Authentication/Update-CIPPSAMRedirectUri.ps1 b/Modules/CIPPCore/Public/Authentication/Update-CIPPSAMRedirectUri.ps1 index 356e922c876d..d6ed545baf51 100644 --- a/Modules/CIPPCore/Public/Authentication/Update-CIPPSAMRedirectUri.ps1 +++ b/Modules/CIPPCore/Public/Authentication/Update-CIPPSAMRedirectUri.ps1 @@ -29,7 +29,7 @@ function Update-CIPPSAMRedirectUri { ) try { - $AppResponse = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/applications(appId='$($env:ApplicationID)')?`$select=id,web" -tenantid $env:TenantID -NoAuthCheck $true + $AppResponse = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/applications(appId='$($env:ApplicationID)')?`$select=id,web" -tenantid $env:TenantID -NoAuthCheck $true -AsApp $true $ExistingUris = @($AppResponse.web.redirectUris) $MissingUris = $RequiredUris | Where-Object { $_ -notin $ExistingUris } @@ -46,7 +46,7 @@ function Update-CIPPSAMRedirectUri { web = @{ redirectUris = $UpdatedUris } } | ConvertTo-Json -Depth 5 - New-GraphPOSTRequest -uri "https://graph.microsoft.com/v1.0/applications/$($AppResponse.id)" -body $Body -tenantid $env:TenantID -type PATCH -NoAuthCheck $true + New-GraphPOSTRequest -uri "https://graph.microsoft.com/v1.0/applications/$($AppResponse.id)" -body $Body -tenantid $env:TenantID -type PATCH -NoAuthCheck $true -AsApp $true Write-Information "[SAM-Redirect] Added redirect URIs: $($MissingUris -join ', ')" Write-LogMessage -API 'SAM-Redirect' -message "Added redirect URIs: $($MissingUris -join ', ')" -sev Info } catch { From d7cda8a309a9c133e98de54257482bb4ef399abc Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Mon, 18 May 2026 07:57:41 -0400 Subject: [PATCH 009/202] Update Initialize-CIPPAuth.ps1 --- .../Authentication/Initialize-CIPPAuth.ps1 | 53 ++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Authentication/Initialize-CIPPAuth.ps1 b/Modules/CIPPCore/Public/Authentication/Initialize-CIPPAuth.ps1 index f5f48e325add..9f618899c237 100644 --- a/Modules/CIPPCore/Public/Authentication/Initialize-CIPPAuth.ps1 +++ b/Modules/CIPPCore/Public/Authentication/Initialize-CIPPAuth.ps1 @@ -108,7 +108,58 @@ function Initialize-CIPPAuth { } } - # 5. Post-migration cleanup: if CIPP_SSO_MIGRATION_APPID is still set but EasyAuth + # 5. Reconcile EasyAuth issuer with SSOMultiTenant setting: if EasyAuth is already + # configured, check whether the issuer URL matches the current SSOMultiTenant value + # from Key Vault. If it changed (e.g. toggled from single to multi-tenant), update + # the EasyAuth config via ARM and restart. + if ($EasyAuthEnabled -and $AuthState.HasSAMCredentials -and -not $env:CIPP_SSO_MIGRATION_APPID) { + try { + $AuthConfigJson = $env:WEBSITE_AUTH_V2_CONFIG_JSON + if ($AuthConfigJson) { + $AuthConfig = $AuthConfigJson | ConvertFrom-Json -ErrorAction Stop + $CurrentIssuer = $AuthConfig.identityProviders.azureActiveDirectory.registration.openIdIssuer + $ConfiguredAppId = $AuthConfig.identityProviders.azureActiveDirectory.registration.clientId + + if ($CurrentIssuer -and $ConfiguredAppId) { + # Read SSOMultiTenant from KV/DevSecrets + $SSOMultiTenant = $false + if ($env:AzureWebJobsStorage -eq 'UseDevelopmentStorage=true' -or $env:NonLocalHostAzurite -eq 'true') { + try { + $DevSecretsTable = Get-CIPPTable -tablename 'DevSecrets' + $Secret = Get-CIPPAzDataTableEntity @DevSecretsTable -Filter "PartitionKey eq 'SSO' and RowKey eq 'SSO'" -ErrorAction SilentlyContinue + $SSOMultiTenant = $Secret.SSOMultiTenant -eq 'True' + } catch { } + } elseif ($KVName) { + try { + $MtVal = Get-CippKeyVaultSecret -VaultName $KVName -Name 'SSOMultiTenant' -AsPlainText -ErrorAction Stop + $SSOMultiTenant = $MtVal -eq 'True' + } catch { } + } + + $ExpectedIssuer = if ($SSOMultiTenant) { + 'https://login.microsoftonline.com/common/v2.0' + } else { + "https://login.microsoftonline.com/$($env:TenantID)/v2.0" + } + + if ($CurrentIssuer -ne $ExpectedIssuer) { + Write-Information "[Auth-Init] EasyAuth issuer mismatch: current=$CurrentIssuer expected=$ExpectedIssuer — updating" + $Configured = Set-CIPPSSOEasyAuth -AppId $ConfiguredAppId -MultiTenant $SSOMultiTenant -TenantId $env:TenantID + if ($Configured) { + Write-Information '[Auth-Init] EasyAuth issuer updated — requesting container restart' + [Craft.Services.AppLifecycleBridge]::RequestRestart('EasyAuth issuer updated to match SSOMultiTenant setting during warmup') + } + } else { + Write-Information "[Auth-Init] EasyAuth issuer matches SSOMultiTenant setting ($SSOMultiTenant) — no update needed" + } + } + } + } catch { + Write-Information "[Auth-Init] EasyAuth issuer reconciliation failed (non-fatal): $_" + } + } + + # 6. Post-migration cleanup: if CIPP_SSO_MIGRATION_APPID is still set but EasyAuth # is now configured, check whether the EasyAuth clientId still matches the migration # app. If it differs, the customer's own CIPP-SSO app is active and we can remove # the migration trigger env var. From ab5e5155681acb8e14994a80fd287a17f1aa3544 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Mon, 18 May 2026 09:24:48 -0400 Subject: [PATCH 010/202] Switch to app auth for authentication changes standard --- Modules/CIPPCore/Public/Set-CIPPAuthenticationPolicy.ps1 | 4 ++-- ...nvoke-CIPPStandardPWdisplayAppInformationRequiredState.ps1 | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Modules/CIPPCore/Public/Set-CIPPAuthenticationPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPAuthenticationPolicy.ps1 index 75d7971388a4..b3fedd2310ab 100644 --- a/Modules/CIPPCore/Public/Set-CIPPAuthenticationPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPAuthenticationPolicy.ps1 @@ -21,7 +21,7 @@ function Set-CIPPAuthenticationPolicy { $State = if ($Enabled) { 'enabled' } else { 'disabled' } # Get current state of the called authentication method and Set state of authentication method to input state try { - $CurrentInfo = New-GraphGetRequest -Uri "https://graph.microsoft.com/beta/policies/authenticationmethodspolicy/authenticationMethodConfigurations/$AuthenticationMethodId" -tenantid $Tenant + $CurrentInfo = New-GraphGetRequest -Uri "https://graph.microsoft.com/beta/policies/authenticationmethodspolicy/authenticationMethodConfigurations/$AuthenticationMethodId" -tenantid $Tenant -AsApp $True $CurrentInfo.state = $State } catch { $ErrorMessage = Get-CippException -Exception $_ @@ -137,7 +137,7 @@ function Set-CIPPAuthenticationPolicy { try { if ($PSCmdlet.ShouldProcess($AuthenticationMethodId, "Set state to $State $OptionalLogMessage")) { # Convert body to JSON and send request - $null = New-GraphPostRequest -tenantid $Tenant -Uri "https://graph.microsoft.com/beta/policies/authenticationmethodspolicy/authenticationMethodConfigurations/$AuthenticationMethodId" -Type PATCH -Body (ConvertTo-Json -InputObject $CurrentInfo -Compress -Depth 10) -ContentType 'application/json' + $null = New-GraphPostRequest -tenantid $Tenant -Uri "https://graph.microsoft.com/beta/policies/authenticationmethodspolicy/authenticationMethodConfigurations/$AuthenticationMethodId" -Type PATCH -Body (ConvertTo-Json -InputObject $CurrentInfo -Compress -Depth 10) -ContentType 'application/json' -AsApp $True Write-LogMessage -headers $Headers -API $APIName -tenant $Tenant -message "Set $AuthenticationMethodId state to $State $OptionalLogMessage" -sev Info } return "Set $AuthenticationMethodId state to $State $OptionalLogMessage" diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardPWdisplayAppInformationRequiredState.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardPWdisplayAppInformationRequiredState.ps1 index f698d0753527..6f40f3945f2f 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardPWdisplayAppInformationRequiredState.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardPWdisplayAppInformationRequiredState.ps1 @@ -48,7 +48,7 @@ function Invoke-CIPPStandardPWdisplayAppInformationRequiredState { param($Tenant, $Settings) try { - $CurrentState = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/policies/authenticationMethodsPolicy/authenticationMethodConfigurations/microsoftAuthenticator' -tenantid $Tenant + $CurrentState = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/policies/authenticationMethodsPolicy/authenticationMethodConfigurations/microsoftAuthenticator' -tenantid $Tenant -AsApp $True } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the PWdisplayAppInformationRequiredState state for $Tenant. Error: $ErrorMessage" -Sev Error From 1b1ee689e9442896d9767f1cba736d6aa8b5d56f Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 19 May 2026 07:20:16 -0400 Subject: [PATCH 011/202] cache PowerShell enabled status and use cached data for standard --- .../DBCache/Set-CIPPDBCacheMailboxes.ps1 | 34 ++++++++++++------- ...tandardDisableExchangeOnlinePowerShell.ps1 | 4 +-- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheMailboxes.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheMailboxes.ps1 index 5a75850f7412..4da4e1784514 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheMailboxes.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheMailboxes.ps1 @@ -25,19 +25,26 @@ function Set-CIPPDBCacheMailboxes { try { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching mailboxes' -sev Debug - # Get mailboxes with select properties - $Select = 'id,ExchangeGuid,ArchiveGuid,UserPrincipalName,DisplayName,PrimarySMTPAddress,RecipientType,RecipientTypeDetails,EmailAddresses,WhenSoftDeleted,IsInactiveMailbox,ForwardingSmtpAddress,DeliverToMailboxAndForward,ForwardingAddress,HiddenFromAddressListsEnabled,ExternalDirectoryObjectId,MessageCopyForSendOnBehalfEnabled,MessageCopyForSentAsEnabled,GrantSendOnBehalfTo,PersistedCapabilities,LitigationHoldEnabled,LitigationHoldDate,LitigationHoldDuration,ComplianceTagHoldApplied,RetentionHoldEnabled,InPlaceHolds,RetentionPolicy' - $ExoRequest = @{ - tenantid = $TenantFilter - cmdlet = 'Get-Mailbox' - cmdParams = @{} - Select = $Select + # Get mailboxes and user details in a single bulk request + $Select = 'id,ExchangeGuid,ArchiveGuid,UserPrincipalName,DisplayName,PrimarySMTPAddress,RecipientType,RecipientTypeDetails,EmailAddresses,WhenSoftDeleted,IsInactiveMailbox,ForwardingSmtpAddress,DeliverToMailboxAndForward,ForwardingAddress,HiddenFromAddressListsEnabled,ExternalDirectoryObjectId,MessageCopyForSendOnBehalfEnabled,MessageCopyForSentAsEnabled,GrantSendOnBehalfTo,PersistedCapabilities,LitigationHoldEnabled,LitigationHoldDate,LitigationHoldDuration,ComplianceTagHoldApplied,RetentionHoldEnabled,InPlaceHolds,RetentionPolicy,RemotePowerShellEnabled,Guid,Identity' + $BulkRequests = @( + @{ CmdletInput = @{ CmdletName = 'Get-Mailbox'; Parameters = @{} } } + @{ CmdletInput = @{ CmdletName = 'Get-User'; Parameters = @{} } } + ) + $BulkResults = New-ExoBulkRequest -tenantid $TenantFilter -cmdletArray $BulkRequests -useSystemMailbox $true -Select $Select -ReturnWithCommand $true + + # Build a lookup hashtable from Get-User results for O(1) matching + $UserLookup = @{} + foreach ($User in @($BulkResults.'Get-User')) { + if ($User.ExternalDirectoryObjectId) { + $UserLookup[$User.ExternalDirectoryObjectId] = $User + } } - # Use Generic List for better memory efficiency with large datasets - $Mailboxes = [System.Collections.Generic.List[PSObject]]::new() - $RawMailboxes = New-ExoRequest @ExoRequest - foreach ($Mailbox in $RawMailboxes) { + # Transform Get-Mailbox results and merge Get-User properties + $Mailboxes = [System.Collections.Generic.List[PSObject]]::new() + foreach ($Mailbox in @($BulkResults.'Get-Mailbox')) { + $MatchedUser = $UserLookup[$Mailbox.ExternalDirectoryObjectId] $Mailboxes.Add(($Mailbox | Select-Object id, ExchangeGuid, ArchiveGuid, WhenSoftDeleted, @{ Name = 'UPN'; Expression = { $_.'UserPrincipalName' } }, @{ Name = 'displayName'; Expression = { $_.'DisplayName' } }, @@ -60,7 +67,10 @@ function Set-CIPPDBCacheMailboxes { RetentionHoldEnabled, InPlaceHolds, RetentionPolicy, - GrantSendOnBehalfTo)) + GrantSendOnBehalfTo, + @{ Name = 'RemotePowerShellEnabled'; Expression = { $MatchedUser.RemotePowerShellEnabled } }, + @{ Name = 'Guid'; Expression = { $MatchedUser.Guid } }, + @{ Name = 'Identity'; Expression = { $MatchedUser.Identity } })) } $Mailboxes | Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Mailboxes' -AddCount diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableExchangeOnlinePowerShell.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableExchangeOnlinePowerShell.ps1 index c801a9924e67..e19319160827 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableExchangeOnlinePowerShell.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableExchangeOnlinePowerShell.ps1 @@ -66,7 +66,7 @@ function Invoke-CIPPStandardDisableExchangeOnlinePowerShell { } $AdminUsers = @($DirectAdminUPNs) + @($GroupMemberUPNs) | Where-Object { $_ } | Select-Object -Unique - $UsersWithPowerShell = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-User' -Select 'userPrincipalName, identity, guid, remotePowerShellEnabled' | Where-Object { $_.RemotePowerShellEnabled -eq $true -and $_.userPrincipalName -notin $AdminUsers } + $UsersWithPowerShell = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Mailboxes' | Where-Object { $_.RemotePowerShellEnabled -eq $true -and $_.UPN -notin $AdminUsers } $PowerShellEnabledCount = ($UsersWithPowerShell | Measure-Object).Count $StateIsCorrect = $PowerShellEnabledCount -eq 0 } catch { @@ -83,7 +83,7 @@ function Invoke-CIPPStandardDisableExchangeOnlinePowerShell { @{ CmdletInput = @{ CmdletName = 'Set-User' - Parameters = @{Identity = $User.Guid; RemotePowerShellEnabled = $false } + Parameters = @{Identity = $User.Guid ?? $User.UPN; RemotePowerShellEnabled = $false } } } } From 6b8ebd4814625fa4902844dca619cb6deb1720eb Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 19 May 2026 07:40:37 -0400 Subject: [PATCH 012/202] refactor calls to use new onepass method to store DB data --- .../DBCache/Set-CIPPDBCacheAdminConsentRequestPolicy.ps1 | 3 +-- .../Public/DBCache/Set-CIPPDBCacheAppRoleAssignments.ps1 | 3 +-- Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheApps.ps1 | 3 +-- .../DBCache/Set-CIPPDBCacheAuthenticationFlowsPolicy.ps1 | 3 +-- .../Set-CIPPDBCacheAuthenticationMethodsPolicy.ps1 | 3 +-- .../DBCache/Set-CIPPDBCacheAuthorizationPolicy.ps1 | 3 +-- .../DBCache/Set-CIPPDBCacheB2BManagementPolicy.ps1 | 3 +-- .../Public/DBCache/Set-CIPPDBCacheBitlockerKeys.ps1 | 3 +-- .../DBCache/Set-CIPPDBCacheConditionalAccessPolicies.ps1 | 9 +++------ .../DBCache/Set-CIPPDBCacheCopilotReadinessActivity.ps1 | 3 +-- .../DBCache/Set-CIPPDBCacheCopilotUsageUserDetail.ps1 | 5 ++--- .../DBCache/Set-CIPPDBCacheCopilotUserCountSummary.ps1 | 5 ++--- .../DBCache/Set-CIPPDBCacheCopilotUserCountTrend.ps1 | 5 ++--- .../Set-CIPPDBCacheCredentialUserRegistrationDetails.ps1 | 3 +-- .../DBCache/Set-CIPPDBCacheCrossTenantAccessPolicy.ps1 | 3 +-- .../DBCache/Set-CIPPDBCacheCsExternalAccessPolicy.ps1 | 3 +-- .../Set-CIPPDBCacheCsTeamsAppPermissionPolicy.ps1 | 3 +-- .../Set-CIPPDBCacheCsTeamsClientConfiguration.ps1 | 3 +-- .../DBCache/Set-CIPPDBCacheCsTeamsMeetingPolicy.ps1 | 3 +-- .../DBCache/Set-CIPPDBCacheCsTeamsMessagingPolicy.ps1 | 3 +-- .../Set-CIPPDBCacheCsTenantFederationConfiguration.ps1 | 3 +-- .../Set-CIPPDBCacheDefaultAppManagementPolicy.ps1 | 3 +-- .../Public/DBCache/Set-CIPPDBCacheDetectedApps.ps1 | 9 +++------ .../DBCache/Set-CIPPDBCacheDeviceRegistrationPolicy.ps1 | 3 +-- .../Public/DBCache/Set-CIPPDBCacheDeviceSettings.ps1 | 3 +-- Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheDevices.ps1 | 3 +-- .../DBCache/Set-CIPPDBCacheDirectoryRecommendations.ps1 | 3 +-- .../DBCache/Set-CIPPDBCacheDlpCompliancePolicies.ps1 | 3 +-- Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheDomains.ps1 | 3 +-- .../Public/DBCache/Set-CIPPDBCacheExoAcceptedDomains.ps1 | 3 +-- .../DBCache/Set-CIPPDBCacheExoAdminAuditLogConfig.ps1 | 3 +-- .../DBCache/Set-CIPPDBCacheExoAntiPhishPolicies.ps1 | 6 ++---- .../DBCache/Set-CIPPDBCacheExoAtpPolicyForO365.ps1 | 3 +-- .../DBCache/Set-CIPPDBCacheExoDkimSigningConfig.ps1 | 3 +-- .../Set-CIPPDBCacheExoHostedContentFilterPolicy.ps1 | 3 +-- .../Set-CIPPDBCacheExoHostedOutboundSpamFilterPolicy.ps1 | 3 +-- .../DBCache/Set-CIPPDBCacheExoMalwareFilterPolicies.ps1 | 6 ++---- .../DBCache/Set-CIPPDBCacheExoOrganizationConfig.ps1 | 3 +-- .../DBCache/Set-CIPPDBCacheExoPresetSecurityPolicy.ps1 | 3 +-- .../DBCache/Set-CIPPDBCacheExoQuarantinePolicy.ps1 | 3 +-- .../Public/DBCache/Set-CIPPDBCacheExoRemoteDomain.ps1 | 3 +-- .../DBCache/Set-CIPPDBCacheExoSafeAttachmentPolicies.ps1 | 6 ++---- .../DBCache/Set-CIPPDBCacheExoSafeLinksPolicies.ps1 | 6 ++---- .../Public/DBCache/Set-CIPPDBCacheExoSharingPolicy.ps1 | 3 +-- .../DBCache/Set-CIPPDBCacheExoTenantAllowBlockList.ps1 | 6 ++---- .../Public/DBCache/Set-CIPPDBCacheExoTransportRules.ps1 | 3 +-- Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheGroups.ps1 | 6 ++---- Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheGuests.ps1 | 3 +-- .../Set-CIPPDBCacheIntuneAppProtectionPolicies.ps1 | 9 +++------ .../Public/DBCache/Set-CIPPDBCacheIntuneApplications.ps1 | 6 ++---- .../DBCache/Set-CIPPDBCacheIntuneAssignmentFilters.ps1 | 3 +-- .../DBCache/Set-CIPPDBCacheIntuneCompliancePolicies.ps1 | 6 ++---- .../Public/DBCache/Set-CIPPDBCacheIntunePolicies.ps1 | 3 +-- .../DBCache/Set-CIPPDBCacheIntuneReusableSettings.ps1 | 3 +-- .../Public/DBCache/Set-CIPPDBCacheIntuneScripts.ps1 | 6 ++---- .../Public/DBCache/Set-CIPPDBCacheLicenseOverview.ps1 | 3 +-- .../Public/DBCache/Set-CIPPDBCacheMDEOnboarding.ps1 | 3 +-- .../CIPPDB/Public/DBCache/Set-CIPPDBCacheMFAState.ps1 | 3 +-- .../Public/DBCache/Set-CIPPDBCacheMailboxUsage.ps1 | 3 +-- .../Set-CIPPDBCacheManagedDeviceEncryptionStates.ps1 | 3 +-- .../Public/DBCache/Set-CIPPDBCacheManagedDevices.ps1 | 3 +-- .../DBCache/Set-CIPPDBCacheOAuth2PermissionGrants.ps1 | 3 +-- .../Public/DBCache/Set-CIPPDBCacheOfficeActivations.ps1 | 3 +-- .../Public/DBCache/Set-CIPPDBCacheOneDriveUsage.ps1 | 6 ++---- .../Public/DBCache/Set-CIPPDBCacheOrganization.ps1 | 3 +-- .../Public/DBCache/Set-CIPPDBCacheOwaMailboxPolicy.ps1 | 3 +-- .../CIPPDB/Public/DBCache/Set-CIPPDBCachePIMSettings.ps1 | 6 ++---- .../DBCache/Set-CIPPDBCacheReportSubmissionPolicy.ps1 | 3 +-- .../Public/DBCache/Set-CIPPDBCacheRiskDetections.ps1 | 3 +-- .../DBCache/Set-CIPPDBCacheRiskyServicePrincipals.ps1 | 3 +-- .../CIPPDB/Public/DBCache/Set-CIPPDBCacheRiskyUsers.ps1 | 3 +-- .../Set-CIPPDBCacheRoleAssignmentScheduleInstances.ps1 | 3 +-- .../DBCache/Set-CIPPDBCacheRoleEligibilitySchedules.ps1 | 3 +-- .../DBCache/Set-CIPPDBCacheRoleManagementPolicies.ps1 | 3 +-- Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheRoles.ps1 | 6 ++---- .../CIPPDB/Public/DBCache/Set-CIPPDBCacheSPOTenant.ps1 | 3 +-- .../Set-CIPPDBCacheSPOTenantSyncClientRestriction.ps1 | 3 +-- .../Public/DBCache/Set-CIPPDBCacheSensitivityLabels.ps1 | 3 +-- .../Set-CIPPDBCacheServicePrincipalRiskDetections.ps1 | 3 +-- .../Public/DBCache/Set-CIPPDBCacheServicePrincipals.ps1 | 3 +-- .../CIPPDB/Public/DBCache/Set-CIPPDBCacheSettings.ps1 | 3 +-- .../DBCache/Set-CIPPDBCacheSharePointSiteUsage.ps1 | 6 ++---- Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheTeams.ps1 | 3 +-- .../Public/DBCache/Set-CIPPDBCacheTeamsActivity.ps1 | 3 +-- .../CIPPDB/Public/DBCache/Set-CIPPDBCacheTeamsVoice.ps1 | 3 +-- .../DBCache/Set-CIPPDBCacheUserRegistrationDetails.ps1 | 3 +-- 86 files changed, 108 insertions(+), 213 deletions(-) diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheAdminConsentRequestPolicy.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheAdminConsentRequestPolicy.ps1 index b14ae9e2113d..66f32dbde1c0 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheAdminConsentRequestPolicy.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheAdminConsentRequestPolicy.ps1 @@ -19,8 +19,7 @@ function Set-CIPPDBCacheAdminConsentRequestPolicy { try { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching admin consent request policy' -sev Debug $ConsentPolicy = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/adminConsentRequestPolicy' -tenantid $TenantFilter - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'AdminConsentRequestPolicy' -Data @($ConsentPolicy) - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'AdminConsentRequestPolicy' -Data @($ConsentPolicy) -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'AdminConsentRequestPolicy' -Data @($ConsentPolicy) -AddCount $ConsentPolicy = $null Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached admin consent request policy successfully' -sev Debug diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheAppRoleAssignments.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheAppRoleAssignments.ps1 index 93433d6c27f9..6db0d82a8769 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheAppRoleAssignments.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheAppRoleAssignments.ps1 @@ -39,8 +39,7 @@ function Set-CIPPDBCacheAppRoleAssignments { } if ($AllAppRoleAssignments.Count -gt 0) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'AppRoleAssignments' -Data $AllAppRoleAssignments - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'AppRoleAssignments' -Data $AllAppRoleAssignments -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'AppRoleAssignments' -Data $AllAppRoleAssignments -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AllAppRoleAssignments.Count) app role assignments" -sev Debug } $AllAppRoleAssignments = $null diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheApps.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheApps.ps1 index ef8acc12acf4..56b65c6c07d1 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheApps.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheApps.ps1 @@ -21,8 +21,7 @@ function Set-CIPPDBCacheApps { $Apps = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/applications?$top=999&expand=owners' -tenantid $TenantFilter if (!$Apps) { $Apps = @() } - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Apps' -Data $Apps - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Apps' -Data $Apps -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Apps' -Data $Apps -AddCount $Apps = $null Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached applications successfully' -sev Debug diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheAuthenticationFlowsPolicy.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheAuthenticationFlowsPolicy.ps1 index cac9230929b9..d25be4186873 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheAuthenticationFlowsPolicy.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheAuthenticationFlowsPolicy.ps1 @@ -22,8 +22,7 @@ function Set-CIPPDBCacheAuthenticationFlowsPolicy { $AuthFlowPolicy = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/authenticationFlowsPolicy' -tenantid $TenantFilter -AsApp $true if ($AuthFlowPolicy) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'AuthenticationFlowsPolicy' -Data @($AuthFlowPolicy) - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'AuthenticationFlowsPolicy' -Data @($AuthFlowPolicy) -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'AuthenticationFlowsPolicy' -Data @($AuthFlowPolicy) -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached authentication flows policy successfully' -sev Debug } diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheAuthenticationMethodsPolicy.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheAuthenticationMethodsPolicy.ps1 index 2ae7c00b416b..e1b63f15b8ca 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheAuthenticationMethodsPolicy.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheAuthenticationMethodsPolicy.ps1 @@ -19,8 +19,7 @@ function Set-CIPPDBCacheAuthenticationMethodsPolicy { try { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching authentication methods policy' -sev Debug $AuthMethodsPolicy = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/authenticationMethodsPolicy' -tenantid $TenantFilter - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'AuthenticationMethodsPolicy' -Data @($AuthMethodsPolicy) - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'AuthenticationMethodsPolicy' -Data @($AuthMethodsPolicy) -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'AuthenticationMethodsPolicy' -Data @($AuthMethodsPolicy) -AddCount $AuthMethodsPolicy = $null Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached authentication methods policy successfully' -sev Debug diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheAuthorizationPolicy.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheAuthorizationPolicy.ps1 index 6aedd8b765c4..7811b92867b9 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheAuthorizationPolicy.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheAuthorizationPolicy.ps1 @@ -19,8 +19,7 @@ function Set-CIPPDBCacheAuthorizationPolicy { try { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching authorization policy' -sev Debug $AuthPolicy = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/authorizationPolicy' -tenantid $TenantFilter - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'AuthorizationPolicy' -Data @($AuthPolicy) - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'AuthorizationPolicy' -Data @($AuthPolicy) -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'AuthorizationPolicy' -Data @($AuthPolicy) -AddCount $AuthPolicy = $null Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached authorization policy successfully' -sev Debug diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheB2BManagementPolicy.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheB2BManagementPolicy.ps1 index b85af4dac1c1..0a3b901af6f2 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheB2BManagementPolicy.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheB2BManagementPolicy.ps1 @@ -23,8 +23,7 @@ function Set-CIPPDBCacheB2BManagementPolicy { $B2BManagementPolicy = $LegacyPolicies if ($B2BManagementPolicy) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'B2BManagementPolicy' -Data @($B2BManagementPolicy) - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'B2BManagementPolicy' -Data @($B2BManagementPolicy) -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'B2BManagementPolicy' -Data @($B2BManagementPolicy) -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached B2B management policy successfully' -sev Debug } else { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No B2B management policy found' -sev Debug diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheBitlockerKeys.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheBitlockerKeys.ps1 index 8bb2bf9971fd..71b70fd9a250 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheBitlockerKeys.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheBitlockerKeys.ps1 @@ -21,8 +21,7 @@ function Set-CIPPDBCacheBitlockerKeys { $BitlockerKeys = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/informationProtection/bitlocker/recoveryKeys' -tenantid $TenantFilter if (!$BitlockerKeys) { $BitlockerKeys = @() } - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'BitlockerKeys' -Data $BitlockerKeys - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'BitlockerKeys' -Data $BitlockerKeys -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'BitlockerKeys' -Data $BitlockerKeys -AddCount $BitlockerKeys = $null Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached BitLocker recovery keys successfully' -sev Debug diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheConditionalAccessPolicies.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheConditionalAccessPolicies.ps1 index d5bd3ec19cde..a4407e0b82d3 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheConditionalAccessPolicies.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheConditionalAccessPolicies.ps1 @@ -29,8 +29,7 @@ function Set-CIPPDBCacheConditionalAccessPolicies { try { $CAPolicies = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/policies?$top=999' -tenantid $TenantFilter if ($CAPolicies) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ConditionalAccessPolicies' -Data $CAPolicies - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ConditionalAccessPolicies' -Data $CAPolicies -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ConditionalAccessPolicies' -Data $CAPolicies -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($CAPolicies.Count) CA policies" -sev Debug } $CAPolicies = $null @@ -42,8 +41,7 @@ function Set-CIPPDBCacheConditionalAccessPolicies { $NamedLocations = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations?$top=999' -tenantid $TenantFilter if ($NamedLocations) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'NamedLocations' -Data $NamedLocations - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'NamedLocations' -Data $NamedLocations -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'NamedLocations' -Data $NamedLocations -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($NamedLocations.Count) named locations" -sev Debug } $NamedLocations = $null @@ -55,8 +53,7 @@ function Set-CIPPDBCacheConditionalAccessPolicies { $AuthStrengths = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/authenticationStrength/policies' -tenantid $TenantFilter if ($AuthStrengths) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'AuthenticationStrengths' -Data $AuthStrengths - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'AuthenticationStrengths' -Data $AuthStrengths -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'AuthenticationStrengths' -Data $AuthStrengths -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AuthStrengths.Count) authentication strengths" -sev Debug } $AuthStrengths = $null diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCopilotReadinessActivity.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCopilotReadinessActivity.ps1 index c055e34ef989..48131fe4b39b 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCopilotReadinessActivity.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCopilotReadinessActivity.ps1 @@ -44,8 +44,7 @@ function Set-CIPPDBCacheCopilotReadinessActivity { } } - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CopilotReadinessActivity' -Data $FlattenedData - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CopilotReadinessActivity' -Data $FlattenedData -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CopilotReadinessActivity' -Data $FlattenedData -AddCount $FlattenedData = $null Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached Copilot readiness activity successfully' -sev Debug diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCopilotUsageUserDetail.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCopilotUsageUserDetail.ps1 index 7f3be5239050..04757c42bda0 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCopilotUsageUserDetail.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCopilotUsageUserDetail.ps1 @@ -22,12 +22,11 @@ function Set-CIPPDBCacheCopilotUsageUserDetail { $Data = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/reports/getMicrosoft365CopilotUsageUserDetail(period='D30')" -tenantid $TenantFilter -AsApp $true if ($Data) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CopilotUsageUserDetail' -Data $Data - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CopilotUsageUserDetail' -Data $Data -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CopilotUsageUserDetail' -Data $Data -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($Data.Count) Copilot usage user detail records" -sev Debug } else { # Write an empty marker so tests can distinguish "no data yet" from "cache not run" - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CopilotUsageUserDetail' -Data @() -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CopilotUsageUserDetail' -Data @() -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Copilot usage user detail: no records returned (no active Copilot usage)' -sev Debug } diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCopilotUserCountSummary.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCopilotUserCountSummary.ps1 index a667626c32b4..4fc0c9f40265 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCopilotUserCountSummary.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCopilotUserCountSummary.ps1 @@ -22,11 +22,10 @@ function Set-CIPPDBCacheCopilotUserCountSummary { $Data = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/reports/getMicrosoft365CopilotUserCountSummary(period='D30')" -tenantid $TenantFilter -AsApp $true if ($Data) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CopilotUserCountSummary' -Data $Data - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CopilotUserCountSummary' -Data $Data -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CopilotUserCountSummary' -Data $Data -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached Copilot user count summary' -sev Debug } else { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CopilotUserCountSummary' -Data @() -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CopilotUserCountSummary' -Data @() -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Copilot user count summary: no records returned (no active Copilot usage)' -sev Debug } diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCopilotUserCountTrend.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCopilotUserCountTrend.ps1 index 6f6034b958c3..227402889db4 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCopilotUserCountTrend.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCopilotUserCountTrend.ps1 @@ -22,11 +22,10 @@ function Set-CIPPDBCacheCopilotUserCountTrend { $Data = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/reports/getMicrosoft365CopilotUserCountTrend(period='D7')?`$format=application/json" -tenantid $TenantFilter -AsApp $true if ($Data) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CopilotUserCountTrend' -Data $Data - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CopilotUserCountTrend' -Data $Data -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CopilotUserCountTrend' -Data $Data -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($Data.Count) Copilot user count trend records" -sev Debug } else { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CopilotUserCountTrend' -Data @() -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CopilotUserCountTrend' -Data @() -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Copilot user count trend: no records returned (no active Copilot usage)' -sev Debug } diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCredentialUserRegistrationDetails.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCredentialUserRegistrationDetails.ps1 index 5242dffdbbf7..198e467ebcc2 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCredentialUserRegistrationDetails.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCredentialUserRegistrationDetails.ps1 @@ -22,8 +22,7 @@ function Set-CIPPDBCacheCredentialUserRegistrationDetails { $CredentialUserRegistrationDetails = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/reports/credentialUserRegistrationDetails' -tenantid $TenantFilter if ($CredentialUserRegistrationDetails) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CredentialUserRegistrationDetails' -Data $CredentialUserRegistrationDetails - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CredentialUserRegistrationDetails' -Data $CredentialUserRegistrationDetails -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CredentialUserRegistrationDetails' -Data $CredentialUserRegistrationDetails -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($CredentialUserRegistrationDetails.Count) credential user registration details" -sev Debug } $CredentialUserRegistrationDetails = $null diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCrossTenantAccessPolicy.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCrossTenantAccessPolicy.ps1 index 1578be33762c..beeacb65cfba 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCrossTenantAccessPolicy.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCrossTenantAccessPolicy.ps1 @@ -19,8 +19,7 @@ function Set-CIPPDBCacheCrossTenantAccessPolicy { try { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching cross-tenant access policy' -sev Debug $CrossTenantPolicy = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/crossTenantAccessPolicy/default' -tenantid $TenantFilter - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CrossTenantAccessPolicy' -Data @($CrossTenantPolicy) - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CrossTenantAccessPolicy' -Data @($CrossTenantPolicy) -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CrossTenantAccessPolicy' -Data @($CrossTenantPolicy) -AddCount $CrossTenantPolicy = $null Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached cross-tenant access policy successfully' -sev Debug diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCsExternalAccessPolicy.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCsExternalAccessPolicy.ps1 index 9889b37b7a6c..96afe6b7eab9 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCsExternalAccessPolicy.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCsExternalAccessPolicy.ps1 @@ -28,8 +28,7 @@ function Set-CIPPDBCacheCsExternalAccessPolicy { if ($ExternalAccess) { $Data = @($ExternalAccess) - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CsExternalAccessPolicy' -Data $Data - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CsExternalAccessPolicy' -Data $Data -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CsExternalAccessPolicy' -Data $Data -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached Teams External Access Policy' -sev Debug } $ExternalAccess = $null diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCsTeamsAppPermissionPolicy.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCsTeamsAppPermissionPolicy.ps1 index 206c1a806741..66c2a171aeca 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCsTeamsAppPermissionPolicy.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCsTeamsAppPermissionPolicy.ps1 @@ -28,8 +28,7 @@ function Set-CIPPDBCacheCsTeamsAppPermissionPolicy { if ($AppPermissionPolicies) { $Data = @($AppPermissionPolicies) - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CsTeamsAppPermissionPolicy' -Data $Data - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CsTeamsAppPermissionPolicy' -Data $Data -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CsTeamsAppPermissionPolicy' -Data $Data -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($Data.Count) Teams App Permission Policies" -sev Debug } $AppPermissionPolicies = $null diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCsTeamsClientConfiguration.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCsTeamsClientConfiguration.ps1 index de1d018ff9c9..1db2ec626fe9 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCsTeamsClientConfiguration.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCsTeamsClientConfiguration.ps1 @@ -29,8 +29,7 @@ function Set-CIPPDBCacheCsTeamsClientConfiguration { if ($ClientConfig) { $Data = @($ClientConfig) - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CsTeamsClientConfiguration' -Data $Data - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CsTeamsClientConfiguration' -Data $Data -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CsTeamsClientConfiguration' -Data $Data -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached Teams Client Configuration' -sev Debug } $ClientConfig = $null diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCsTeamsMeetingPolicy.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCsTeamsMeetingPolicy.ps1 index 7dcea7b968ee..5a67acdb1ddb 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCsTeamsMeetingPolicy.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCsTeamsMeetingPolicy.ps1 @@ -28,8 +28,7 @@ function Set-CIPPDBCacheCsTeamsMeetingPolicy { if ($MeetingPolicy) { $Data = @($MeetingPolicy) - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CsTeamsMeetingPolicy' -Data $Data - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CsTeamsMeetingPolicy' -Data $Data -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CsTeamsMeetingPolicy' -Data $Data -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached Teams Meeting Policy' -sev Debug } $MeetingPolicy = $null diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCsTeamsMessagingPolicy.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCsTeamsMessagingPolicy.ps1 index e3696593c918..2fc1ef784c23 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCsTeamsMessagingPolicy.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCsTeamsMessagingPolicy.ps1 @@ -29,8 +29,7 @@ function Set-CIPPDBCacheCsTeamsMessagingPolicy { if ($MessagingPolicy) { $Data = @($MessagingPolicy) - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CsTeamsMessagingPolicy' -Data $Data - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CsTeamsMessagingPolicy' -Data $Data -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CsTeamsMessagingPolicy' -Data $Data -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached Teams Messaging Policy' -sev Debug } $MessagingPolicy = $null diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCsTenantFederationConfiguration.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCsTenantFederationConfiguration.ps1 index b5d76c0f89e5..47d09881239e 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCsTenantFederationConfiguration.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCsTenantFederationConfiguration.ps1 @@ -29,8 +29,7 @@ function Set-CIPPDBCacheCsTenantFederationConfiguration { if ($Federation) { $Data = @($Federation) - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CsTenantFederationConfiguration' -Data $Data - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CsTenantFederationConfiguration' -Data $Data -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CsTenantFederationConfiguration' -Data $Data -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached Teams Tenant Federation Configuration' -sev Debug } $Federation = $null diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheDefaultAppManagementPolicy.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheDefaultAppManagementPolicy.ps1 index c8126a8744d2..56886caa2735 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheDefaultAppManagementPolicy.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheDefaultAppManagementPolicy.ps1 @@ -19,8 +19,7 @@ function Set-CIPPDBCacheDefaultAppManagementPolicy { try { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching default app management policy' -sev Debug $AppMgmtPolicy = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/defaultAppManagementPolicy' -tenantid $TenantFilter - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DefaultAppManagementPolicy' -Data @($AppMgmtPolicy) - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DefaultAppManagementPolicy' -Data @($AppMgmtPolicy) -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DefaultAppManagementPolicy' -Data @($AppMgmtPolicy) -AddCount $AppMgmtPolicy = $null Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached default app management policy successfully' -sev Debug diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheDetectedApps.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheDetectedApps.ps1 index c79b183c09c8..8783690f4b61 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheDetectedApps.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheDetectedApps.ps1 @@ -60,8 +60,7 @@ function Set-CIPPDBCacheDetectedApps { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Retrieved $($DetectedApps.Count) detected apps (expected $TotalCount)" -sev Debug if ($DetectedApps.Count -eq 0) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DetectedApps' -Data @() - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DetectedApps' -Data @() -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DetectedApps' -Data @() -AddCount return } @@ -85,13 +84,11 @@ function Set-CIPPDBCacheDetectedApps { $App } - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DetectedApps' -Data $DetectedAppsWithDevices - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DetectedApps' -Data $DetectedAppsWithDevices -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DetectedApps' -Data $DetectedAppsWithDevices -AddCount $DetectedApps = $null $DetectedAppsWithDevices = $null } else { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DetectedApps' -Data $DetectedApps - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DetectedApps' -Data $DetectedApps -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DetectedApps' -Data $DetectedApps -AddCount $DetectedApps = $null } diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheDeviceRegistrationPolicy.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheDeviceRegistrationPolicy.ps1 index 3ef85cfe05c6..876244bae3dc 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheDeviceRegistrationPolicy.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheDeviceRegistrationPolicy.ps1 @@ -22,8 +22,7 @@ function Set-CIPPDBCacheDeviceRegistrationPolicy { $DeviceRegistrationPolicy = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/deviceRegistrationPolicy' -tenantid $TenantFilter if ($DeviceRegistrationPolicy) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DeviceRegistrationPolicy' -Data @($DeviceRegistrationPolicy) - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DeviceRegistrationPolicy' -Data @($DeviceRegistrationPolicy) -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DeviceRegistrationPolicy' -Data @($DeviceRegistrationPolicy) -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached device registration policy successfully' -sev Debug } diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheDeviceSettings.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheDeviceSettings.ps1 index 2a0b4a480d91..79dd4c4cff82 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheDeviceSettings.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheDeviceSettings.ps1 @@ -20,8 +20,7 @@ function Set-CIPPDBCacheDeviceSettings { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching device settings' -sev Debug $DeviceSettings = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/directory/deviceLocalCredentials' -tenantid $TenantFilter - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DeviceSettings' -Data @($DeviceSettings) - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DeviceSettings' -Data @($DeviceSettings) -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DeviceSettings' -Data @($DeviceSettings) -AddCount $DeviceSettings = $null Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached device settings successfully' -sev Debug diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheDevices.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheDevices.ps1 index c0d056617209..cb2bec81698f 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheDevices.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheDevices.ps1 @@ -21,8 +21,7 @@ function Set-CIPPDBCacheDevices { $Devices = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/devices?$top=999&$select=id,displayName,operatingSystem,operatingSystemVersion,trustType,accountEnabled,approximateLastSignInDateTime' -tenantid $TenantFilter if (!$Devices) { $Devices = @() } - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Devices' -Data $Devices - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Devices' -Data $Devices -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Devices' -Data $Devices -AddCount $Devices = $null Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached Azure AD devices successfully' -sev Debug diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheDirectoryRecommendations.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheDirectoryRecommendations.ps1 index 1a1e973cd695..75074c9ab3b4 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheDirectoryRecommendations.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheDirectoryRecommendations.ps1 @@ -20,8 +20,7 @@ function Set-CIPPDBCacheDirectoryRecommendations { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching directory recommendations' -sev Debug $Recommendations = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/directory/recommendations?$top=999' -tenantid $TenantFilter - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DirectoryRecommendations' -Data $Recommendations - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DirectoryRecommendations' -Data $Recommendations -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DirectoryRecommendations' -Data $Recommendations -AddCount $Recommendations = $null Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached directory recommendations successfully' -sev Debug diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheDlpCompliancePolicies.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheDlpCompliancePolicies.ps1 index 776b868f163d..13d9a9927377 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheDlpCompliancePolicies.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheDlpCompliancePolicies.ps1 @@ -30,8 +30,7 @@ function Set-CIPPDBCacheDlpCompliancePolicies { $Policies = New-ExoRequest -TenantId $Tenant.customerId -cmdlet 'Get-DlpCompliancePolicy' -Compliance -Select 'Name,DisplayName,Mode,Enabled,Workload,CreatedBy,WhenCreatedUTC,WhenChangedUTC' if ($Policies) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DlpCompliancePolicies' -Data $Policies - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DlpCompliancePolicies' -Data $Policies -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DlpCompliancePolicies' -Data $Policies -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($Policies.Count) DLP compliance policies" -sev Debug } diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheDomains.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheDomains.ps1 index 405342b9eefa..0a67432e77b8 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheDomains.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheDomains.ps1 @@ -19,8 +19,7 @@ function Set-CIPPDBCacheDomains { try { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching domains' -sev Debug $Domains = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/domains' -tenantid $TenantFilter - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Domains' -Data @($Domains) - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Domains' -Data @($Domains) -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Domains' -Data @($Domains) -AddCount $Domains = $null Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached domains successfully' -sev Debug diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoAcceptedDomains.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoAcceptedDomains.ps1 index b2af3cdbe3b8..be47cf4c3714 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoAcceptedDomains.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoAcceptedDomains.ps1 @@ -22,8 +22,7 @@ function Set-CIPPDBCacheExoAcceptedDomains { $AcceptedDomains = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-AcceptedDomain' if ($AcceptedDomains) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoAcceptedDomains' -Data $AcceptedDomains - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoAcceptedDomains' -Data $AcceptedDomains -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoAcceptedDomains' -Data $AcceptedDomains -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AcceptedDomains.Count) Accepted Domains" -sev Debug } $AcceptedDomains = $null diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoAdminAuditLogConfig.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoAdminAuditLogConfig.ps1 index 2c0b2a10e863..d51a81c26ba2 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoAdminAuditLogConfig.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoAdminAuditLogConfig.ps1 @@ -24,8 +24,7 @@ function Set-CIPPDBCacheExoAdminAuditLogConfig { if ($AuditConfig) { # AdminAuditLogConfig returns a single object, wrap in array for consistency $AuditConfigArray = @($AuditConfig) - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoAdminAuditLogConfig' -Data $AuditConfigArray - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoAdminAuditLogConfig' -Data $AuditConfigArray -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoAdminAuditLogConfig' -Data $AuditConfigArray -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached Exchange Admin Audit Log configuration' -sev Debug } $AuditConfig = $null diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoAntiPhishPolicies.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoAntiPhishPolicies.ps1 index f35ba843e566..2e7fa2dbee1b 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoAntiPhishPolicies.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoAntiPhishPolicies.ps1 @@ -22,8 +22,7 @@ function Set-CIPPDBCacheExoAntiPhishPolicies { # Get Anti-Phishing policies $AntiPhishPolicies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-AntiPhishPolicy' if ($AntiPhishPolicies) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoAntiPhishPolicies' -Data $AntiPhishPolicies - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoAntiPhishPolicies' -Data $AntiPhishPolicies -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoAntiPhishPolicies' -Data $AntiPhishPolicies -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AntiPhishPolicies.Count) Anti-Phishing policies" -sev Debug } $AntiPhishPolicies = $null @@ -31,8 +30,7 @@ function Set-CIPPDBCacheExoAntiPhishPolicies { # Get Anti-Phishing rules $AntiPhishRules = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-AntiPhishRule' if ($AntiPhishRules) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoAntiPhishRules' -Data $AntiPhishRules - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoAntiPhishRules' -Data $AntiPhishRules -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoAntiPhishRules' -Data $AntiPhishRules -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AntiPhishRules.Count) Anti-Phishing rules" -sev Debug } $AntiPhishRules = $null diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoAtpPolicyForO365.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoAtpPolicyForO365.ps1 index c48fd4baab3a..b93320868475 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoAtpPolicyForO365.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoAtpPolicyForO365.ps1 @@ -21,8 +21,7 @@ function Set-CIPPDBCacheExoAtpPolicyForO365 { $AtpPolicies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-AtpPolicyForO365' if ($AtpPolicies) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoAtpPolicyForO365' -Data $AtpPolicies - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoAtpPolicyForO365' -Data $AtpPolicies -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoAtpPolicyForO365' -Data $AtpPolicies -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AtpPolicies.Count) ATP policies for Office 365" -sev Debug } $AtpPolicies = $null diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoDkimSigningConfig.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoDkimSigningConfig.ps1 index aa7ea3bd4561..a0e3b6b2350a 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoDkimSigningConfig.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoDkimSigningConfig.ps1 @@ -22,8 +22,7 @@ function Set-CIPPDBCacheExoDkimSigningConfig { $DkimConfig = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-DkimSigningConfig' if ($DkimConfig) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoDkimSigningConfig' -Data $DkimConfig - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoDkimSigningConfig' -Data $DkimConfig -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoDkimSigningConfig' -Data $DkimConfig -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($DkimConfig.Count) DKIM configurations" -sev Debug } $DkimConfig = $null diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoHostedContentFilterPolicy.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoHostedContentFilterPolicy.ps1 index cd71f9a00bc5..958648690c0e 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoHostedContentFilterPolicy.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoHostedContentFilterPolicy.ps1 @@ -20,8 +20,7 @@ function Set-CIPPDBCacheExoHostedContentFilterPolicy { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Hosted Content Filter policies' -sev Debug $HostedContentFilterPolicies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-HostedContentFilterPolicy' if ($HostedContentFilterPolicies) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoHostedContentFilterPolicy' -Data $HostedContentFilterPolicies - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoHostedContentFilterPolicy' -Data $HostedContentFilterPolicies -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoHostedContentFilterPolicy' -Data $HostedContentFilterPolicies -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($HostedContentFilterPolicies.Count) Hosted Content Filter policies" -sev Debug } $HostedContentFilterPolicies = $null diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoHostedOutboundSpamFilterPolicy.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoHostedOutboundSpamFilterPolicy.ps1 index a550c9031c8e..2a53545a2405 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoHostedOutboundSpamFilterPolicy.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoHostedOutboundSpamFilterPolicy.ps1 @@ -21,8 +21,7 @@ function Set-CIPPDBCacheExoHostedOutboundSpamFilterPolicy { $HostedOutboundSpamFilterPolicies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-HostedOutboundSpamFilterPolicy' if ($HostedOutboundSpamFilterPolicies) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoHostedOutboundSpamFilterPolicy' -Data $HostedOutboundSpamFilterPolicies - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoHostedOutboundSpamFilterPolicy' -Data $HostedOutboundSpamFilterPolicies -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoHostedOutboundSpamFilterPolicy' -Data $HostedOutboundSpamFilterPolicies -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($HostedOutboundSpamFilterPolicies.Count) Hosted Outbound Spam Filter policies" -sev Debug } $HostedOutboundSpamFilterPolicies = $null diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoMalwareFilterPolicies.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoMalwareFilterPolicies.ps1 index 23f6b1f775ef..7d71ab954e5e 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoMalwareFilterPolicies.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoMalwareFilterPolicies.ps1 @@ -22,8 +22,7 @@ function Set-CIPPDBCacheExoMalwareFilterPolicies { # Get Malware Filter policies $MalwarePolicies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-MalwareFilterPolicy' if ($MalwarePolicies) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoMalwareFilterPolicies' -Data $MalwarePolicies - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoMalwareFilterPolicies' -Data $MalwarePolicies -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoMalwareFilterPolicies' -Data $MalwarePolicies -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($MalwarePolicies.Count) Malware Filter policies" -sev Debug } $MalwarePolicies = $null @@ -31,8 +30,7 @@ function Set-CIPPDBCacheExoMalwareFilterPolicies { # Get Malware Filter rules $MalwareRules = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-MalwareFilterRule' if ($MalwareRules) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoMalwareFilterRules' -Data $MalwareRules - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoMalwareFilterRules' -Data $MalwareRules -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoMalwareFilterRules' -Data $MalwareRules -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($MalwareRules.Count) Malware Filter rules" -sev Debug } $MalwareRules = $null diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoOrganizationConfig.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoOrganizationConfig.ps1 index 27a8c481a7da..c07c203edaef 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoOrganizationConfig.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoOrganizationConfig.ps1 @@ -24,8 +24,7 @@ function Set-CIPPDBCacheExoOrganizationConfig { if ($OrgConfig) { # OrganizationConfig returns a single object, wrap in array for consistency $OrgConfigArray = @($OrgConfig) - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoOrganizationConfig' -Data $OrgConfigArray - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoOrganizationConfig' -Data $OrgConfigArray -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoOrganizationConfig' -Data $OrgConfigArray -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached Exchange Organization configuration' -sev Debug } $OrgConfig = $null diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoPresetSecurityPolicy.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoPresetSecurityPolicy.ps1 index 0810cf69ade7..7930ec943f18 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoPresetSecurityPolicy.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoPresetSecurityPolicy.ps1 @@ -38,8 +38,7 @@ function Set-CIPPDBCacheExoPresetSecurityPolicy { } if ($AllRules.Count -gt 0) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoPresetSecurityPolicy' -Data $AllRules - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoPresetSecurityPolicy' -Data $AllRules -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoPresetSecurityPolicy' -Data $AllRules -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AllRules.Count) Preset Security Policy rules" -sev Debug } $EOPRules = $null diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoQuarantinePolicy.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoQuarantinePolicy.ps1 index b4bcdd7ac011..cd7fb1afbbd1 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoQuarantinePolicy.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoQuarantinePolicy.ps1 @@ -21,8 +21,7 @@ function Set-CIPPDBCacheExoQuarantinePolicy { $QuarantinePolicies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-QuarantinePolicy' if ($QuarantinePolicies) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoQuarantinePolicy' -Data $QuarantinePolicies - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoQuarantinePolicy' -Data $QuarantinePolicies -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoQuarantinePolicy' -Data $QuarantinePolicies -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($QuarantinePolicies.Count) Quarantine policies" -sev Debug } $QuarantinePolicies = $null diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoRemoteDomain.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoRemoteDomain.ps1 index e9b32337a9ee..dd9fb277b99a 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoRemoteDomain.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoRemoteDomain.ps1 @@ -21,8 +21,7 @@ function Set-CIPPDBCacheExoRemoteDomain { $RemoteDomains = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-RemoteDomain' if ($RemoteDomains) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoRemoteDomain' -Data $RemoteDomains - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoRemoteDomain' -Data $RemoteDomains -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoRemoteDomain' -Data $RemoteDomains -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($RemoteDomains.Count) Remote Domains" -sev Debug } $RemoteDomains = $null diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoSafeAttachmentPolicies.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoSafeAttachmentPolicies.ps1 index 28d8f587afc3..07867a4caff3 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoSafeAttachmentPolicies.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoSafeAttachmentPolicies.ps1 @@ -22,8 +22,7 @@ function Set-CIPPDBCacheExoSafeAttachmentPolicies { # Get Safe Attachment policies $SafeAttachmentPolicies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-SafeAttachmentPolicy' if ($SafeAttachmentPolicies) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoSafeAttachmentPolicies' -Data $SafeAttachmentPolicies - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoSafeAttachmentPolicies' -Data $SafeAttachmentPolicies -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoSafeAttachmentPolicies' -Data $SafeAttachmentPolicies -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($SafeAttachmentPolicies.Count) Safe Attachment policies" -sev Debug } $SafeAttachmentPolicies = $null @@ -31,8 +30,7 @@ function Set-CIPPDBCacheExoSafeAttachmentPolicies { # Get Safe Attachment rules $SafeAttachmentRules = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-SafeAttachmentRule' if ($SafeAttachmentRules) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoSafeAttachmentRules' -Data $SafeAttachmentRules - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoSafeAttachmentRules' -Data $SafeAttachmentRules -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoSafeAttachmentRules' -Data $SafeAttachmentRules -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($SafeAttachmentRules.Count) Safe Attachment rules" -sev Debug } $SafeAttachmentRules = $null diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoSafeLinksPolicies.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoSafeLinksPolicies.ps1 index f78468844344..65403b187268 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoSafeLinksPolicies.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoSafeLinksPolicies.ps1 @@ -22,8 +22,7 @@ function Set-CIPPDBCacheExoSafeLinksPolicies { # Get Safe Links policies $SafeLinksPolicies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-SafeLinksPolicy' if ($SafeLinksPolicies) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoSafeLinksPolicies' -Data $SafeLinksPolicies - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoSafeLinksPolicies' -Data $SafeLinksPolicies -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoSafeLinksPolicies' -Data $SafeLinksPolicies -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($SafeLinksPolicies.Count) Safe Links policies" -sev Debug } $SafeLinksPolicies = $null @@ -31,8 +30,7 @@ function Set-CIPPDBCacheExoSafeLinksPolicies { # Get Safe Links rules $SafeLinksRules = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-SafeLinksRule' if ($SafeLinksRules) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoSafeLinksRules' -Data $SafeLinksRules - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoSafeLinksRules' -Data $SafeLinksRules -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoSafeLinksRules' -Data $SafeLinksRules -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($SafeLinksRules.Count) Safe Links rules" -sev Debug } $SafeLinksRules = $null diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoSharingPolicy.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoSharingPolicy.ps1 index ffaaed92ed0a..368c1dca953c 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoSharingPolicy.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoSharingPolicy.ps1 @@ -22,8 +22,7 @@ function Set-CIPPDBCacheExoSharingPolicy { $SharingPolicies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-SharingPolicy' if ($SharingPolicies) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoSharingPolicy' -Data $SharingPolicies - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoSharingPolicy' -Data $SharingPolicies -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoSharingPolicy' -Data $SharingPolicies -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($SharingPolicies.Count) Sharing Policies" -sev Debug } $SharingPolicies = $null diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoTenantAllowBlockList.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoTenantAllowBlockList.ps1 index 3906bf3d55f8..a73dfafa7e2e 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoTenantAllowBlockList.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoTenantAllowBlockList.ps1 @@ -36,13 +36,11 @@ function Set-CIPPDBCacheExoTenantAllowBlockList { } if ($AllItems.Count -gt 0) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoTenantAllowBlockList' -Data $AllItems - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoTenantAllowBlockList' -Data $AllItems -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoTenantAllowBlockList' -Data $AllItems -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AllItems.Count) Tenant Allow/Block List items" -sev Debug } else { # Even if empty, store an empty array so test knows cache was populated - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoTenantAllowBlockList' -Data @() - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoTenantAllowBlockList' -Data @() -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoTenantAllowBlockList' -Data @() -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached empty Tenant Allow/Block List' -sev Debug } $SenderItems = $null diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoTransportRules.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoTransportRules.ps1 index f08860ac00c6..a557947d4ce0 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoTransportRules.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoTransportRules.ps1 @@ -22,8 +22,7 @@ function Set-CIPPDBCacheExoTransportRules { $TransportRules = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-TransportRule' if ($TransportRules) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoTransportRules' -Data $TransportRules - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoTransportRules' -Data $TransportRules -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoTransportRules' -Data $TransportRules -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($TransportRules.Count) Transport Rules" -sev Debug } $TransportRules = $null diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheGroups.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheGroups.ps1 index b0857fd24d21..03c843a41ce0 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheGroups.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheGroups.ps1 @@ -59,8 +59,7 @@ function Set-CIPPDBCacheGroups { $Group } - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Groups' -Data $GroupsWithMembers - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Groups' -Data $GroupsWithMembers -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Groups' -Data $GroupsWithMembers -AddCount $Groups = $null $GroupsWithMembers = $null } else { @@ -82,8 +81,7 @@ function Set-CIPPDBCacheGroups { $Group | Add-Member -NotePropertyName 'calculatedGroupType' -NotePropertyValue $calculatedGroupType -Force $Group } - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Groups' -Data $Groups - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Groups' -Data $Groups -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Groups' -Data $Groups -AddCount $Groups = $null } diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheGuests.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheGuests.ps1 index 867d45b791cd..7a25235e6946 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheGuests.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheGuests.ps1 @@ -21,8 +21,7 @@ function Set-CIPPDBCacheGuests { $Guests = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users?`$filter=userType eq 'Guest'&`$expand=sponsors&`$top=999" -tenantid $TenantFilter if (!$Guests) { $Guests = @() } - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Guests' -Data $Guests - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Guests' -Data $Guests -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Guests' -Data $Guests -AddCount $Guests = $null Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached guest users successfully' -sev Debug diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneAppProtectionPolicies.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneAppProtectionPolicies.ps1 index d3dd14ad33fa..418d5a277852 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneAppProtectionPolicies.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneAppProtectionPolicies.ps1 @@ -74,12 +74,9 @@ function Set-CIPPDBCacheIntuneAppProtectionPolicies { if (-not $Groups) { $Groups = @() } if (-not $MobileAppConfigs) { $MobileAppConfigs = @() } - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'IntuneAppProtectionPolicyGroups' -Data @($Groups) - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'IntuneAppProtectionPolicyGroups' -Data @($Groups) -Count - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'IntuneAppProtectionManagedAppPolicies' -Data @($ManagedAppPoliciesWithAssignments) - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'IntuneAppProtectionManagedAppPolicies' -Data @($ManagedAppPoliciesWithAssignments) -Count - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'IntuneAppProtectionMobileAppConfigurations' -Data @($MobileAppConfigs) - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'IntuneAppProtectionMobileAppConfigurations' -Data @($MobileAppConfigs) -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'IntuneAppProtectionPolicyGroups' -Data @($Groups) -AddCount + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'IntuneAppProtectionManagedAppPolicies' -Data @($ManagedAppPoliciesWithAssignments) -AddCount + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'IntuneAppProtectionMobileAppConfigurations' -Data @($MobileAppConfigs) -AddCount $TotalCount = (($ManagedAppPoliciesWithAssignments | Measure-Object).Count + ($MobileAppConfigs | Measure-Object).Count) Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $TotalCount app protection/configuration policies" -sev Debug diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneApplications.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneApplications.ps1 index 811f2af821d3..8594bc640137 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneApplications.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneApplications.ps1 @@ -34,10 +34,8 @@ function Set-CIPPDBCacheIntuneApplications { if (-not $Groups) { $Groups = @() } if (-not $Apps) { $Apps = @() } - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'IntuneApplicationGroups' -Data @($Groups) - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'IntuneApplicationGroups' -Data @($Groups) -Count - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'IntuneApplications' -Data @($Apps) - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'IntuneApplications' -Data @($Apps) -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'IntuneApplicationGroups' -Data @($Groups) -AddCount + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'IntuneApplications' -Data @($Apps) -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $(($Apps | Measure-Object).Count) Intune applications" -sev Debug } catch { diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneAssignmentFilters.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneAssignmentFilters.ps1 index 882021cfb4ea..e6037b1f1061 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneAssignmentFilters.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneAssignmentFilters.ps1 @@ -17,8 +17,7 @@ function Set-CIPPDBCacheIntuneAssignmentFilters { $AssignmentFilters = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/assignmentFilters' -tenantid $TenantFilter if (-not $AssignmentFilters) { $AssignmentFilters = @() } - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'IntuneAssignmentFilters' -Data @($AssignmentFilters) - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'IntuneAssignmentFilters' -Data @($AssignmentFilters) -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'IntuneAssignmentFilters' -Data @($AssignmentFilters) -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $(($AssignmentFilters | Measure-Object).Count) assignment filters" -sev Debug } catch { diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneCompliancePolicies.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneCompliancePolicies.ps1 index 80256d0ce92c..04ea2689982f 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneCompliancePolicies.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneCompliancePolicies.ps1 @@ -34,10 +34,8 @@ function Set-CIPPDBCacheIntuneCompliancePolicies { if (-not $Groups) { $Groups = @() } if (-not $Policies) { $Policies = @() } - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'IntuneCompliancePolicyGroups' -Data @($Groups) - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'IntuneCompliancePolicyGroups' -Data @($Groups) -Count - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'IntuneDeviceCompliancePolicies' -Data @($Policies) - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'IntuneDeviceCompliancePolicies' -Data @($Policies) -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'IntuneCompliancePolicyGroups' -Data @($Groups) -AddCount + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'IntuneDeviceCompliancePolicies' -Data @($Policies) -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $(($Policies | Measure-Object).Count) compliance policies" -sev Debug } catch { diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntunePolicies.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntunePolicies.ps1 index 427965a7b3a0..4acc51cee07a 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntunePolicies.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntunePolicies.ps1 @@ -104,8 +104,7 @@ function Set-CIPPDBCacheIntunePolicies { } } - Add-CIPPDbItem -TenantFilter $TenantFilter -Type "Intune$($PolicyType.Type)" -Data $Policies - Add-CIPPDbItem -TenantFilter $TenantFilter -Type "Intune$($PolicyType.Type)" -Data $Policies -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type "Intune$($PolicyType.Type)" -Data $Policies -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($Policies.Count) $($PolicyType.Type)" -sev Debug # Fetch device statuses for compliance policies using bulk requests diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneReusableSettings.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneReusableSettings.ps1 index 77a964147403..437bcd6607db 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneReusableSettings.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneReusableSettings.ps1 @@ -29,8 +29,7 @@ function Set-CIPPDBCacheIntuneReusableSettings { $Settings = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/reusablePolicySettings$SelectQuery" -tenantid $TenantFilter if (-not $Settings) { $Settings = @() } - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'IntuneReusableSettings' -Data @($Settings) - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'IntuneReusableSettings' -Data @($Settings) -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'IntuneReusableSettings' -Data @($Settings) -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $(($Settings | Measure-Object).Count) reusable settings" -sev Debug } catch { diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneScripts.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneScripts.ps1 index 8fb780b7d4ff..1052c9505b9d 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneScripts.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneScripts.ps1 @@ -46,8 +46,7 @@ function Set-CIPPDBCacheIntuneScripts { $Groups = ($BulkResults | Where-Object { $_.id -eq 'Groups' }).body.value if (-not $Groups) { $Groups = @() } - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'IntuneScriptGroups' -Data @($Groups) - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'IntuneScriptGroups' -Data @($Groups) -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'IntuneScriptGroups' -Data @($Groups) -AddCount $TypeMap = @{ Windows = 'IntuneWindowsScripts' @@ -75,8 +74,7 @@ function Set-CIPPDBCacheIntuneScripts { }) } - Add-CIPPDbItem -TenantFilter $TenantFilter -Type $TypeMap[$scriptId] -Data @($Scripts) - Add-CIPPDbItem -TenantFilter $TenantFilter -Type $TypeMap[$scriptId] -Data @($Scripts) -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type $TypeMap[$scriptId] -Data @($Scripts) -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $(($Scripts | Measure-Object).Count) $scriptId scripts" -sev Debug } } catch { diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheLicenseOverview.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheLicenseOverview.ps1 index cb28201d7746..57b31b3702c1 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheLicenseOverview.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheLicenseOverview.ps1 @@ -20,8 +20,7 @@ function Set-CIPPDBCacheLicenseOverview { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching license overview' -sev Debug $LicenseOverview = Get-CIPPLicenseOverview -TenantFilter $TenantFilter - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'LicenseOverview' -Data @($LicenseOverview) - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'LicenseOverview' -Data @($LicenseOverview) -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'LicenseOverview' -Data @($LicenseOverview) -AddCount $LicenseOverview = $null Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached license overview successfully' -sev Debug diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheMDEOnboarding.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheMDEOnboarding.ps1 index 6a0db7384460..ea251b0c8ff7 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheMDEOnboarding.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheMDEOnboarding.ps1 @@ -37,8 +37,7 @@ function Set-CIPPDBCacheMDEOnboarding { ) } - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MDEOnboarding' -Data @($Result) - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MDEOnboarding' -Data @($Result) -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MDEOnboarding' -Data @($Result) -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached MDE onboarding status successfully' -sev Debug } catch { diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheMFAState.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheMFAState.ps1 index ccd489fe14ce..6b3bfa8cf340 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheMFAState.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheMFAState.ps1 @@ -20,8 +20,7 @@ function Set-CIPPDBCacheMFAState { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching MFA state' -sev Debug $MFAState = Get-CIPPMFAState -TenantFilter $TenantFilter - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MFAState' -Data @($MFAState) - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MFAState' -Data @($MFAState) -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MFAState' -Data @($MFAState) -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($MFAState.Count) MFA state records successfully" -sev Debug diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheMailboxUsage.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheMailboxUsage.ps1 index bd13f0b2da02..1488af0eda4c 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheMailboxUsage.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheMailboxUsage.ps1 @@ -20,8 +20,7 @@ function Set-CIPPDBCacheMailboxUsage { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching mailbox usage' -sev Debug $MailboxUsage = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/reports/getMailboxUsageDetail(period='D7')?`$format=application%2fjson" -tenantid $TenantFilter - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MailboxUsage' -Data $MailboxUsage - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MailboxUsage' -Data $MailboxUsage -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MailboxUsage' -Data $MailboxUsage -AddCount $MailboxUsage = $null Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached mailbox usage successfully' -sev Debug diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheManagedDeviceEncryptionStates.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheManagedDeviceEncryptionStates.ps1 index 7dd8764854ff..23f28f1f673e 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheManagedDeviceEncryptionStates.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheManagedDeviceEncryptionStates.ps1 @@ -22,8 +22,7 @@ function Set-CIPPDBCacheManagedDeviceEncryptionStates { $ManagedDeviceEncryptionStates = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/managedDeviceEncryptionStates?$top=999' -tenantid $TenantFilter if ($ManagedDeviceEncryptionStates) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ManagedDeviceEncryptionStates' -Data $ManagedDeviceEncryptionStates - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ManagedDeviceEncryptionStates' -Data $ManagedDeviceEncryptionStates -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ManagedDeviceEncryptionStates' -Data $ManagedDeviceEncryptionStates -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($ManagedDeviceEncryptionStates.Count) managed device encryption states" -sev Debug } $ManagedDeviceEncryptionStates = $null diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheManagedDevices.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheManagedDevices.ps1 index 5190fd72c24f..5c462e1d91e3 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheManagedDevices.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheManagedDevices.ps1 @@ -20,8 +20,7 @@ function Set-CIPPDBCacheManagedDevices { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching managed devices' -sev Debug $ManagedDevices = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/managedDevices?$top=999' -tenantid $TenantFilter if (!$ManagedDevices) { $ManagedDevices = @() } - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ManagedDevices' -Data $ManagedDevices - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ManagedDevices' -Data $ManagedDevices -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ManagedDevices' -Data $ManagedDevices -AddCount $ManagedDevices = $null Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached managed devices successfully' -sev Debug diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheOAuth2PermissionGrants.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheOAuth2PermissionGrants.ps1 index 6fe33e93d378..404d7c7189ad 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheOAuth2PermissionGrants.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheOAuth2PermissionGrants.ps1 @@ -22,8 +22,7 @@ function Set-CIPPDBCacheOAuth2PermissionGrants { $OAuth2PermissionGrants = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/oauth2PermissionGrants?$top=999' -tenantid $TenantFilter if ($OAuth2PermissionGrants) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'OAuth2PermissionGrants' -Data $OAuth2PermissionGrants - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'OAuth2PermissionGrants' -Data $OAuth2PermissionGrants -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'OAuth2PermissionGrants' -Data $OAuth2PermissionGrants -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($OAuth2PermissionGrants.Count) OAuth2 permission grants" -sev Debug } $OAuth2PermissionGrants = $null diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheOfficeActivations.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheOfficeActivations.ps1 index 208b4ce0261b..272f1fc317d6 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheOfficeActivations.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheOfficeActivations.ps1 @@ -20,8 +20,7 @@ function Set-CIPPDBCacheOfficeActivations { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Office activations' -sev Debug $Activations = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/reports/getOffice365ActivationsUserDetail?`$format=application%2fjson" -tenantid $TenantFilter - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'OfficeActivations' -Data $Activations - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'OfficeActivations' -Data $Activations -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'OfficeActivations' -Data $Activations -AddCount $Activations = $null Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached Office activations successfully' -sev Debug diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheOneDriveUsage.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheOneDriveUsage.ps1 index 4dedf324a0c2..2000025ebd5d 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheOneDriveUsage.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheOneDriveUsage.ps1 @@ -56,11 +56,9 @@ function Set-CIPPDBCacheOneDriveUsage { }) } - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'OneDriveSiteListing' -Data @($OneDriveListing) - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'OneDriveSiteListing' -Data @($OneDriveListing) -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'OneDriveSiteListing' -Data @($OneDriveListing) -AddCount - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'OneDriveUsage' -Data @($OneDriveUsage) - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'OneDriveUsage' -Data @($OneDriveUsage) -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'OneDriveUsage' -Data @($OneDriveUsage) -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached OneDrive site listing and usage successfully' -sev Debug diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheOrganization.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheOrganization.ps1 index a5e8607ab14f..80b4dea03bf4 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheOrganization.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheOrganization.ps1 @@ -20,8 +20,7 @@ function Set-CIPPDBCacheOrganization { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching organization data' -sev Debug $Organization = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/organization' -tenantid $TenantFilter - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Organization' -Data $Organization - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Organization' -Data $Organization -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Organization' -Data $Organization -AddCount $Organization = $null Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached organization data successfully' -sev Debug diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheOwaMailboxPolicy.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheOwaMailboxPolicy.ps1 index 6bb55b05780f..b29ad6bfa0e9 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheOwaMailboxPolicy.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheOwaMailboxPolicy.ps1 @@ -28,8 +28,7 @@ function Set-CIPPDBCacheOwaMailboxPolicy { $OwaMailboxPolicies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-OwaMailboxPolicy' if ($OwaMailboxPolicies) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'OwaMailboxPolicy' -Data $OwaMailboxPolicies - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'OwaMailboxPolicy' -Data $OwaMailboxPolicies -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'OwaMailboxPolicy' -Data $OwaMailboxPolicies -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($OwaMailboxPolicies.Count) OWA Mailbox Policies" -sev Debug } $OwaMailboxPolicies = $null diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCachePIMSettings.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCachePIMSettings.ps1 index d5da952cec5c..01057595b165 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCachePIMSettings.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCachePIMSettings.ps1 @@ -30,8 +30,7 @@ function Set-CIPPDBCachePIMSettings { $PIMRoleSettings = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/roleManagementPolicyAssignments?$top=999' -tenantid $TenantFilter if ($PIMRoleSettings) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'PIMRoleSettings' -Data $PIMRoleSettings - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'PIMRoleSettings' -Data $PIMRoleSettings -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'PIMRoleSettings' -Data $PIMRoleSettings -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($PIMRoleSettings.Count) PIM role settings" -sev Debug } $PIMRoleSettings = $null @@ -43,8 +42,7 @@ function Set-CIPPDBCachePIMSettings { $PIMAssignments = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/roleManagement/directory/roleEligibilityScheduleInstances?$top=999' -tenantid $TenantFilter if ($PIMAssignments) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'PIMAssignments' -Data $PIMAssignments - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'PIMAssignments' -Data $PIMAssignments -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'PIMAssignments' -Data $PIMAssignments -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($PIMAssignments.Count) PIM assignments" -sev Debug } $PIMAssignments = $null diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheReportSubmissionPolicy.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheReportSubmissionPolicy.ps1 index b3de00a9591b..fb0e24beae0c 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheReportSubmissionPolicy.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheReportSubmissionPolicy.ps1 @@ -28,8 +28,7 @@ function Set-CIPPDBCacheReportSubmissionPolicy { if ($ReportSubmissionPolicies) { $Data = @($ReportSubmissionPolicies) - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ReportSubmissionPolicy' -Data $Data - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ReportSubmissionPolicy' -Data $Data -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ReportSubmissionPolicy' -Data $Data -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($Data.Count) Report Submission Policies" -sev Debug } $ReportSubmissionPolicies = $null diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheRiskDetections.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheRiskDetections.ps1 index f7878e2f9edb..6abdfd46ff95 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheRiskDetections.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheRiskDetections.ps1 @@ -23,8 +23,7 @@ function Set-CIPPDBCacheRiskDetections { $RiskDetections = New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/identityProtection/riskDetections' -tenantid $TenantFilter if ($RiskDetections) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'RiskDetections' -Data $RiskDetections - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'RiskDetections' -Data $RiskDetections -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'RiskDetections' -Data $RiskDetections -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($RiskDetections.Count) risk detections successfully" -sev Debug } else { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No risk detections found or Identity Protection not available' -sev Debug diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheRiskyServicePrincipals.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheRiskyServicePrincipals.ps1 index 4efcfce693a1..921208f6fff3 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheRiskyServicePrincipals.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheRiskyServicePrincipals.ps1 @@ -23,8 +23,7 @@ function Set-CIPPDBCacheRiskyServicePrincipals { $RiskyServicePrincipals = New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/identityProtection/riskyServicePrincipals' -tenantid $TenantFilter if ($RiskyServicePrincipals) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'RiskyServicePrincipals' -Data $RiskyServicePrincipals - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'RiskyServicePrincipals' -Data $RiskyServicePrincipals -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'RiskyServicePrincipals' -Data $RiskyServicePrincipals -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($RiskyServicePrincipals.Count) risky service principals successfully" -sev Debug } else { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No risky service principals found or Workload Identity Protection not available' -sev Debug diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheRiskyUsers.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheRiskyUsers.ps1 index 6e29032f0b03..7e47e3b66ba5 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheRiskyUsers.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheRiskyUsers.ps1 @@ -23,8 +23,7 @@ function Set-CIPPDBCacheRiskyUsers { $RiskyUsers = New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/identityProtection/riskyUsers' -tenantid $TenantFilter if ($RiskyUsers) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'RiskyUsers' -Data $RiskyUsers - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'RiskyUsers' -Data $RiskyUsers -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'RiskyUsers' -Data $RiskyUsers -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($RiskyUsers.Count) risky users successfully" -sev Debug } else { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No risky users found or Identity Protection not available' -sev Debug diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheRoleAssignmentScheduleInstances.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheRoleAssignmentScheduleInstances.ps1 index aa2b914bf794..04b802332c70 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheRoleAssignmentScheduleInstances.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheRoleAssignmentScheduleInstances.ps1 @@ -19,8 +19,7 @@ function Set-CIPPDBCacheRoleAssignmentScheduleInstances { try { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching role assignment schedule instances' -sev Debug $RoleAssignmentScheduleInstances = New-GraphGetRequest -Uri 'https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignmentScheduleInstances' -tenantid $TenantFilter - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'RoleAssignmentScheduleInstances' -Data @($RoleAssignmentScheduleInstances) - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'RoleAssignmentScheduleInstances' -Data @($RoleAssignmentScheduleInstances) -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'RoleAssignmentScheduleInstances' -Data @($RoleAssignmentScheduleInstances) -AddCount $RoleAssignmentScheduleInstances = $null Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached role assignment schedule instances successfully' -sev Debug diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheRoleEligibilitySchedules.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheRoleEligibilitySchedules.ps1 index 1bcffb691b27..3a3b4208ced5 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheRoleEligibilitySchedules.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheRoleEligibilitySchedules.ps1 @@ -19,8 +19,7 @@ function Set-CIPPDBCacheRoleEligibilitySchedules { try { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching role eligibility schedules' -sev Debug $RoleEligibilitySchedules = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/roleManagement/directory/roleEligibilitySchedules' -tenantid $TenantFilter - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'RoleEligibilitySchedules' -Data @($RoleEligibilitySchedules) - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'RoleEligibilitySchedules' -Data @($RoleEligibilitySchedules) -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'RoleEligibilitySchedules' -Data @($RoleEligibilitySchedules) -AddCount $RoleEligibilitySchedules = $null Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached role eligibility schedules successfully' -sev Debug diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheRoleManagementPolicies.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheRoleManagementPolicies.ps1 index 23c7b5fe5ff8..6d88d55f0259 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheRoleManagementPolicies.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheRoleManagementPolicies.ps1 @@ -19,8 +19,7 @@ function Set-CIPPDBCacheRoleManagementPolicies { try { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching role management policies' -sev Debug $RoleManagementPolicies = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/roleManagementPolicies' -tenantid $TenantFilter - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'RoleManagementPolicies' -Data @($RoleManagementPolicies) - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'RoleManagementPolicies' -Data @($RoleManagementPolicies) -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'RoleManagementPolicies' -Data @($RoleManagementPolicies) -AddCount $RoleManagementPolicies = $null Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached role management policies successfully' -sev Debug diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheRoles.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheRoles.ps1 index bcc10c993b20..3668cdaf18f6 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheRoles.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheRoles.ps1 @@ -49,13 +49,11 @@ function Set-CIPPDBCacheRoles { } } - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Roles' -Data $RolesWithMembers - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Roles' -Data $RolesWithMembers -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Roles' -Data $RolesWithMembers -AddCount $Roles = $null $RolesWithMembers = $null } else { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Roles' -Data $Roles - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Roles' -Data $Roles -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Roles' -Data $Roles -AddCount $Roles = $null } diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheSPOTenant.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheSPOTenant.ps1 index 3181f7399c60..0fcd2cd9f808 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheSPOTenant.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheSPOTenant.ps1 @@ -30,8 +30,7 @@ function Set-CIPPDBCacheSPOTenant { if ($SPOTenant) { $SPOTenantArray = @($SPOTenant) - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'SPOTenant' -Data $SPOTenantArray - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'SPOTenant' -Data $SPOTenantArray -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'SPOTenant' -Data $SPOTenantArray -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached SharePoint Online tenant configuration' -sev Debug } $SPOTenant = $null diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheSPOTenantSyncClientRestriction.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheSPOTenantSyncClientRestriction.ps1 index 1c74c5c3fa3f..65b6b242fa9b 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheSPOTenantSyncClientRestriction.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheSPOTenantSyncClientRestriction.ps1 @@ -39,8 +39,7 @@ function Set-CIPPDBCacheSPOTenantSyncClientRestriction { TenantFilter = $TenantFilter } $Data = @($SyncRestriction) - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'SPOTenantSyncClientRestriction' -Data $Data - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'SPOTenantSyncClientRestriction' -Data $Data -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'SPOTenantSyncClientRestriction' -Data $Data -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached SharePoint sync client restriction' -sev Debug } $SPOTenant = $null diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheSensitivityLabels.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheSensitivityLabels.ps1 index b9d89242742a..307bc5e87913 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheSensitivityLabels.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheSensitivityLabels.ps1 @@ -29,8 +29,7 @@ function Set-CIPPDBCacheSensitivityLabels { $Labels = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/security/informationProtection/sensitivityLabels' -tenantid $TenantFilter -AsApp $true if ($Labels) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'SensitivityLabels' -Data $Labels - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'SensitivityLabels' -Data $Labels -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'SensitivityLabels' -Data $Labels -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($Labels.Count) sensitivity labels" -sev Debug } diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheServicePrincipalRiskDetections.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheServicePrincipalRiskDetections.ps1 index bcd4cfba838e..892e19511701 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheServicePrincipalRiskDetections.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheServicePrincipalRiskDetections.ps1 @@ -23,8 +23,7 @@ function Set-CIPPDBCacheServicePrincipalRiskDetections { $ServicePrincipalRiskDetections = New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/identityProtection/servicePrincipalRiskDetections' -tenantid $TenantFilter if ($ServicePrincipalRiskDetections) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ServicePrincipalRiskDetections' -Data $ServicePrincipalRiskDetections - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ServicePrincipalRiskDetections' -Data $ServicePrincipalRiskDetections -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ServicePrincipalRiskDetections' -Data $ServicePrincipalRiskDetections -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($ServicePrincipalRiskDetections.Count) service principal risk detections successfully" -sev Debug } else { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No service principal risk detections found or Workload Identity Protection not available' -sev Debug diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheServicePrincipals.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheServicePrincipals.ps1 index af887bf31342..166b6cac7fd6 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheServicePrincipals.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheServicePrincipals.ps1 @@ -20,8 +20,7 @@ function Set-CIPPDBCacheServicePrincipals { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching service principals' -sev Debug $ServicePrincipals = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/servicePrincipals' -tenantid $TenantFilter - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ServicePrincipals' -Data $ServicePrincipals - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ServicePrincipals' -Data $ServicePrincipals -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ServicePrincipals' -Data $ServicePrincipals -AddCount $ServicePrincipals = $null Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached service principals successfully' -sev Debug diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheSettings.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheSettings.ps1 index d1951058c3d2..ce2c02bfa7de 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheSettings.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheSettings.ps1 @@ -21,8 +21,7 @@ function Set-CIPPDBCacheSettings { $Settings = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/settings?$top=999' -tenantid $TenantFilter if (!$Settings) { $Settings = @() } - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Settings' -Data $Settings - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Settings' -Data $Settings -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Settings' -Data $Settings -AddCount $Settings = $null Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached directory settings successfully' -sev Debug diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheSharePointSiteUsage.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheSharePointSiteUsage.ps1 index 5722f962b7a0..561a01721236 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheSharePointSiteUsage.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheSharePointSiteUsage.ps1 @@ -90,11 +90,9 @@ function Set-CIPPDBCacheSharePointSiteUsage { $Site.AutoMapUrl = "tenantId=$($TenantId)&webId={$($Site.sharepointIds.webId)}&siteid={$($Site.sharepointIds.siteId)}&webUrl=$($Site.webUrl)&listId={$($ListId)}" } - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'SharePointSiteListing' -Data @($SiteListing) - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'SharePointSiteListing' -Data @($SiteListing) -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'SharePointSiteListing' -Data @($SiteListing) -AddCount - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'SharePointSiteUsage' -Data @($UsageRows) - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'SharePointSiteUsage' -Data @($UsageRows) -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'SharePointSiteUsage' -Data @($UsageRows) -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached SharePoint site listing and usage successfully' -sev Debug diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheTeams.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheTeams.ps1 index ac91707e2cbc..3e0a1e5d69ac 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheTeams.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheTeams.ps1 @@ -12,8 +12,7 @@ function Set-CIPPDBCacheTeams { $Teams = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/groups?`$filter=resourceProvisioningOptions/Any(x:x eq 'Team')&`$select=id,displayName,description,visibility,mailNickname" -tenantid $TenantFilter | Sort-Object -Property displayName - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Teams' -Data @($Teams) - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Teams' -Data @($Teams) -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Teams' -Data @($Teams) -AddCount } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache Teams list: $($_.Exception.Message)" -sev Error -LogData (Get-CippException -Exception $_) } diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheTeamsActivity.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheTeamsActivity.ps1 index a8482de6c30f..1ecb5d42b00b 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheTeamsActivity.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheTeamsActivity.ps1 @@ -18,8 +18,7 @@ function Set-CIPPDBCacheTeamsActivity { @{ Name = 'MeetingCount'; Expression = { $_.'Meeting Count' } } $DbType = "TeamsActivity$Type" - Add-CIPPDbItem -TenantFilter $TenantFilter -Type $DbType -Data @($TeamsActivity) - Add-CIPPDbItem -TenantFilter $TenantFilter -Type $DbType -Data @($TeamsActivity) -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type $DbType -Data @($TeamsActivity) -AddCount } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache Teams activity: $($_.Exception.Message)" -sev Error -LogData (Get-CippException -Exception $_) } diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheTeamsVoice.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheTeamsVoice.ps1 index edc41533d438..37b9be41b2a8 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheTeamsVoice.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheTeamsVoice.ps1 @@ -37,8 +37,7 @@ function Set-CIPPDBCacheTeamsVoice { } while ($Data.Count -eq 999) $PhoneNumbers = @($AllNumbers | Where-Object { $_.TelephoneNumber }) - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'TeamsVoice' -Data $PhoneNumbers - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'TeamsVoice' -Data $PhoneNumbers -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'TeamsVoice' -Data $PhoneNumbers -AddCount } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache Teams Voice phone numbers: $($_.Exception.Message)" -sev Error -LogData (Get-CippException -Exception $_) } diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheUserRegistrationDetails.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheUserRegistrationDetails.ps1 index 64e596f669c9..121f6d62a17b 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheUserRegistrationDetails.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheUserRegistrationDetails.ps1 @@ -22,8 +22,7 @@ function Set-CIPPDBCacheUserRegistrationDetails { $UserRegistrationDetails = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/reports/authenticationMethods/userRegistrationDetails' -tenantid $TenantFilter if ($UserRegistrationDetails) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'UserRegistrationDetails' -Data $UserRegistrationDetails - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'UserRegistrationDetails' -Data $UserRegistrationDetails -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'UserRegistrationDetails' -Data $UserRegistrationDetails -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($UserRegistrationDetails.Count) user registration details" -sev Debug } $UserRegistrationDetails = $null From e3e82cd95b9597706f1edc3ccde5776695abe820 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 19 May 2026 07:51:16 -0400 Subject: [PATCH 013/202] Cache Security Defaults --- Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 | 37 +++++++++++++------ ...t-CIPPDBCacheConditionalAccessPolicies.ps1 | 10 +++++ ...-CIPPStandardConditionalAccessTemplate.ps1 | 24 ++++++------ 3 files changed, 49 insertions(+), 22 deletions(-) diff --git a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 index bed989dc48dc..478bd5e79bf9 100644 --- a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 @@ -12,7 +12,8 @@ function New-CIPPCAPolicy { $APIName = 'Create CA Policy', $Headers, $PreloadedCAPolicies = $null, - $PreloadedLocations = $null + $PreloadedLocations = $null, + $PreloadedSecurityDefaults = $null ) # Helper function to replace group display names with GUIDs @@ -490,16 +491,30 @@ function New-CIPPCAPolicy { } } if ($DisableSD -eq $true) { - #Send request to disable security defaults. - $body = '{ "isEnabled": false }' - try { - $null = New-GraphPostRequest -tenantid $TenantFilter -Uri 'https://graph.microsoft.com/beta/policies/identitySecurityDefaultsEnforcementPolicy' -Type patch -Body $body -asApp $true - Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message "Disabled Security Defaults for tenant $($TenantFilter)" -Sev 'Info' - Start-Sleep 3 - } catch { - $ErrorMessage = Get-CippException -Exception $_ - Write-Information "Error disabling security defaults: $($ErrorMessage | ConvertTo-Json -Depth 10 -Compress)" - Write-Information "Failed to disable security defaults for tenant $($TenantFilter): $($ErrorMessage.NormalizedError)" + # Check if Security Defaults is already disabled using preloaded or live data + $SDPolicy = $PreloadedSecurityDefaults + if ($null -eq $SDPolicy) { + try { + $SDPolicy = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/identitySecurityDefaultsEnforcementPolicy' -tenantid $TenantFilter -AsApp $true + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-Information "Error fetching Security Defaults status: $($ErrorMessage | ConvertTo-Json -Depth 10 -Compress)" + } + } + + if ($SDPolicy.isEnabled -eq $false) { + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message 'Security Defaults already disabled, skipping.' -Sev 'Info' + } else { + $body = '{ "isEnabled": false }' + try { + $null = New-GraphPostRequest -tenantid $TenantFilter -Uri 'https://graph.microsoft.com/beta/policies/identitySecurityDefaultsEnforcementPolicy' -Type patch -Body $body -asApp $true + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message "Disabled Security Defaults for tenant $($TenantFilter)" -Sev 'Info' + Start-Sleep 3 + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-Information "Error disabling security defaults: $($ErrorMessage | ConvertTo-Json -Depth 10 -Compress)" + Write-Information "Failed to disable security defaults for tenant $($TenantFilter): $($ErrorMessage.NormalizedError)" + } } } $RawJSON = ConvertTo-Json -InputObject $JSONobj -Depth 10 -Compress diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheConditionalAccessPolicies.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheConditionalAccessPolicies.ps1 index a4407e0b82d3..7e11accd8824 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheConditionalAccessPolicies.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheConditionalAccessPolicies.ps1 @@ -61,6 +61,16 @@ function Set-CIPPDBCacheConditionalAccessPolicies { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache authentication strengths: $($_.Exception.Message)" -sev Warning } + try { + $SecurityDefaults = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/identitySecurityDefaultsEnforcementPolicy' -tenantid $TenantFilter -AsApp $true + if ($SecurityDefaults) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'SecurityDefaults' -Data @($SecurityDefaults) + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached Security Defaults policy (isEnabled=$($SecurityDefaults.isEnabled))" -sev Debug + } + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache Security Defaults: $($_.Exception.Message)" -sev Warning + } + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached CA data successfully' -sev Debug } catch { diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 index 4d5c8447da02..549396edce9b 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 @@ -60,6 +60,7 @@ function Invoke-CIPPStandardConditionalAccessTemplate { #Get from DB, as we just downloaded the latest before the standard runs. $AllCAPolicies = New-CIPPDbRequest -TenantFilter $tenant -Type 'ConditionalAccessPolicies' $PreloadedLocations = New-CIPPDbRequest -TenantFilter $tenant -Type 'NamedLocations' + $PreloadedSecurityDefaults = New-CIPPDbRequest -TenantFilter $tenant -Type 'SecurityDefaults' } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the ConditionalAccessTemplate state for $Tenant. Error: $ErrorMessage" -Sev Error @@ -80,17 +81,18 @@ function Invoke-CIPPStandardConditionalAccessTemplate { } } $NewCAPolicy = @{ - replacePattern = 'displayName' - TenantFilter = $Tenant - state = $Settings.state - RawJSON = $JSONObj - Overwrite = $true - APIName = 'Standards' - Headers = $Request.Headers - DisableSD = $Settings.DisableSD - CreateGroups = $Settings.CreateGroups ?? $false - PreloadedCAPolicies = $AllCAPolicies - PreloadedLocations = $PreloadedLocations + replacePattern = 'displayName' + TenantFilter = $Tenant + state = $Settings.state + RawJSON = $JSONObj + Overwrite = $true + APIName = 'Standards' + Headers = $Request.Headers + DisableSD = $Settings.DisableSD + CreateGroups = $Settings.CreateGroups ?? $false + PreloadedCAPolicies = $AllCAPolicies + PreloadedLocations = $PreloadedLocations + PreloadedSecurityDefaults = $PreloadedSecurityDefaults } $null = New-CIPPCAPolicy @NewCAPolicy From 9ba48711c614be8e2452ffff2e539b5b1a11b534 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 19 May 2026 10:01:24 -0400 Subject: [PATCH 014/202] correct incorrect default value --- Config/standards.json | 2 +- .../Standards/Invoke-CIPPStandardIntuneComplianceSettings.ps1 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Config/standards.json b/Config/standards.json index 6230b383dab5..ce9752a56d19 100644 --- a/Config/standards.json +++ b/Config/standards.json @@ -3746,7 +3746,7 @@ "type": "number", "name": "standards.IntuneComplianceSettings.deviceComplianceCheckinThresholdDays", "label": "Compliance status validity period (days)", - "defaultValue": 130, + "defaultValue": 120, "validators": { "min": { "value": 1, "message": "Minimum value is 1" }, "max": { "value": 120, "message": "Maximum value is 120" } diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardIntuneComplianceSettings.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardIntuneComplianceSettings.ps1 index 8baa105848d6..ebd612ffac70 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardIntuneComplianceSettings.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardIntuneComplianceSettings.ps1 @@ -17,7 +17,7 @@ function Invoke-CIPPStandardIntuneComplianceSettings { Configures how the system treats devices that don't have specific compliance policies and sets how often devices must check in to maintain their compliance status. This ensures proper security oversight of all corporate devices and maintains current compliance information. ADDEDCOMPONENT {"type":"autoComplete","required":true,"multiple":false,"creatable":false,"name":"standards.IntuneComplianceSettings.secureByDefault","label":"Mark devices with no compliance policy as","options":[{"label":"Compliant","value":"false"},{"label":"Non-Compliant","value":"true"}]} - {"type":"number","name":"standards.IntuneComplianceSettings.deviceComplianceCheckinThresholdDays","label":"Compliance status validity period (days)","defaultValue":130,"validators":{"min":{"value":1,"message":"Minimum value is 1"},"max":{"value":120,"message":"Maximum value is 120"}}} + {"type":"number","name":"standards.IntuneComplianceSettings.deviceComplianceCheckinThresholdDays","label":"Compliance status validity period (days)","defaultValue":120,"validators":{"min":{"value":1,"message":"Minimum value is 1"},"max":{"value":120,"message":"Maximum value is 120"}}} IMPACT Low Impact ADDEDDATE From 73f83719d8d6123db9e450248f55dc97acfdc0ea Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 19 May 2026 10:01:41 -0400 Subject: [PATCH 015/202] add logging to geoip lookip --- Modules/CIPPCore/Public/Get-CIPPGeoIPLocation.ps1 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Get-CIPPGeoIPLocation.ps1 b/Modules/CIPPCore/Public/Get-CIPPGeoIPLocation.ps1 index 754454529e3d..b6eaabf73f71 100644 --- a/Modules/CIPPCore/Public/Get-CIPPGeoIPLocation.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPGeoIPLocation.ps1 @@ -12,7 +12,10 @@ function Get-CIPPGeoIPLocation { return ($GeoIP.Data | ConvertFrom-Json) } $location = Invoke-CIPPRestMethod -Uri "https://geoipdb.azurewebsites.net/api/GetIPInfo?IP=$IP" - if ($location.status -eq 'FAIL') { throw "Could not get location for $IP" } + if ($location.status -eq 'FAIL') { + Write-logMessage -API GeoIPLocation -message "Failed to get location for $IP. API returned status 'FAIL' with message: $($location.message)" -sev Warning + throw "Could not get location for $IP" + } $CacheGeo = @{ PartitionKey = 'IP' RowKey = $IP From 9fce7e77b7d7cf29692fa3aa869a373e076f7658 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Wed, 20 May 2026 22:10:20 -0400 Subject: [PATCH 016/202] feat: add in missing options for Windows Hello standard --- ...ntWindowsHelloForBusinessConfiguration.ps1 | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEnrollmentWindowsHelloForBusinessConfiguration.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEnrollmentWindowsHelloForBusinessConfiguration.ps1 index b3328394d715..806ef7276061 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEnrollmentWindowsHelloForBusinessConfiguration.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEnrollmentWindowsHelloForBusinessConfiguration.ps1 @@ -28,6 +28,8 @@ function Invoke-CIPPStandardEnrollmentWindowsHelloForBusinessConfiguration { {"type":"switch","name":"standards.EnrollmentWindowsHelloForBusinessConfiguration.unlockWithBiometricsEnabled","label":"Allow biometric authentication","default":true} {"type":"autoComplete","name":"standards.EnrollmentWindowsHelloForBusinessConfiguration.enhancedBiometricsState","label":"Use enhanced anti-spoofing when available","multiple":false,"options":[{"label":"Not configured","value":"notConfigured"},{"label":"Enabled","value":"enabled"},{"label":"Disabled","value":"disabled"}]} {"type":"switch","name":"standards.EnrollmentWindowsHelloForBusinessConfiguration.remotePassportEnabled","label":"Allow phone sign-in","default":true} + {"type":"autoComplete","name":"standards.EnrollmentWindowsHelloForBusinessConfiguration.enhancedSignInSecurity","label":"Enable enhanced sign-in security","multiple":false,"options":[{"label":"Not configured","value":"0"},{"label":"Enabled on capable hardware","value":"1"},{"label":"Disabled on all systems","value":"2"}]} + {"type":"autoComplete","name":"standards.EnrollmentWindowsHelloForBusinessConfiguration.securityKeyForSignIn","label":"Use security keys for sign-in","multiple":false,"options":[{"label":"Not configured","value":"notConfigured"},{"label":"Enabled","value":"enabled"},{"label":"Disabled","value":"disabled"}]} IMPACT Low Impact ADDEDDATE @@ -56,13 +58,15 @@ function Invoke-CIPPStandardEnrollmentWindowsHelloForBusinessConfiguration { try { $CurrentState = New-GraphGetRequest -Uri "https://graph.microsoft.com/beta/deviceManagement/deviceEnrollmentConfigurations?`$expand=assignments&orderBy=priority&`$filter=deviceEnrollmentConfigurationType eq 'WindowsHelloForBusiness'" -tenantID $Tenant -AsApp $true | - Select-Object -Property id, pinMinimumLength, pinMaximumLength, pinUppercaseCharactersUsage, pinLowercaseCharactersUsage, pinSpecialCharactersUsage, state, securityDeviceRequired, unlockWithBiometricsEnabled, remotePassportEnabled, pinPreviousBlockCount, pinExpirationInDays, enhancedBiometricsState + Select-Object -Property id, pinMinimumLength, pinMaximumLength, pinUppercaseCharactersUsage, pinLowercaseCharactersUsage, pinSpecialCharactersUsage, state, securityDeviceRequired, unlockWithBiometricsEnabled, remotePassportEnabled, pinPreviousBlockCount, pinExpirationInDays, enhancedBiometricsState, enhancedSignInSecurity, securityKeyForSignIn } catch { - $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the EnrollmentWindowsHelloForBusinessConfiguration state for $Tenant. Error: $ErrorMessage" -Sev Error + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the EnrollmentWindowsHelloForBusinessConfiguration state for $Tenant. Error: $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage return } + $EnhancedSignInSecurity = if ($null -ne $Settings.enhancedSignInSecurity) { [int]$Settings.enhancedSignInSecurity.value } else { $null } + $StateIsCorrect = ($CurrentState.pinMinimumLength -eq $Settings.pinMinimumLength) -and ($CurrentState.pinMaximumLength -eq $Settings.pinMaximumLength) -and ($CurrentState.pinUppercaseCharactersUsage -eq $Settings.pinUppercaseCharactersUsage.value) -and @@ -74,7 +78,10 @@ function Invoke-CIPPStandardEnrollmentWindowsHelloForBusinessConfiguration { ($CurrentState.remotePassportEnabled -eq $Settings.remotePassportEnabled) -and ($CurrentState.pinPreviousBlockCount -eq $Settings.pinPreviousBlockCount) -and ($CurrentState.pinExpirationInDays -eq $Settings.pinExpirationInDays) -and - ($CurrentState.enhancedBiometricsState -eq $Settings.enhancedBiometricsState.value) + ($CurrentState.enhancedBiometricsState -eq $Settings.enhancedBiometricsState.value) -and + (($null -eq $Settings.enhancedSignInSecurity) -or ($CurrentState.enhancedSignInSecurity -eq $EnhancedSignInSecurity)) -and + (($null -eq $Settings.securityKeyForSignIn) -or ($CurrentState.securityKeyForSignIn -eq $Settings.securityKeyForSignIn.value)) + # Backwards compatibility for when newer settings were not yet added $CompareField = [PSCustomObject]@{ pinMinimumLength = $CurrentState.pinMinimumLength @@ -91,6 +98,14 @@ function Invoke-CIPPStandardEnrollmentWindowsHelloForBusinessConfiguration { enhancedBiometricsState = $CurrentState.enhancedBiometricsState } + if ($null -ne $Settings.enhancedSignInSecurity) { + $CompareField | Add-Member -NotePropertyName enhancedSignInSecurity -NotePropertyValue $CurrentState.enhancedSignInSecurity + } + + if ($null -ne $Settings.securityKeyForSignIn) { + $CompareField | Add-Member -NotePropertyName securityKeyForSignIn -NotePropertyValue $CurrentState.securityKeyForSignIn + } + $ExpectedValue = [PSCustomObject]@{ pinMinimumLength = $Settings.pinMinimumLength pinMaximumLength = $Settings.pinMaximumLength @@ -106,6 +121,14 @@ function Invoke-CIPPStandardEnrollmentWindowsHelloForBusinessConfiguration { enhancedBiometricsState = $Settings.enhancedBiometricsState.value } + if ($null -ne $Settings.enhancedSignInSecurity) { + $ExpectedValue | Add-Member -NotePropertyName enhancedSignInSecurity -NotePropertyValue $EnhancedSignInSecurity + } + + if ($null -ne $Settings.securityKeyForSignIn) { + $ExpectedValue | Add-Member -NotePropertyName securityKeyForSignIn -NotePropertyValue $Settings.securityKeyForSignIn.value + } + if ($Settings.remediate -eq $true) { if ($StateIsCorrect -eq $true) { Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'EnrollmentWindowsHelloForBusinessConfiguration is already applied correctly.' -Sev Info @@ -130,6 +153,8 @@ function Invoke-CIPPStandardEnrollmentWindowsHelloForBusinessConfiguration { pinPreviousBlockCount = $Settings.pinPreviousBlockCount pinExpirationInDays = $Settings.pinExpirationInDays enhancedBiometricsState = $Settings.enhancedBiometricsState.value + enhancedSignInSecurity = ($EnhancedSignInSecurity ?? $CurrentState.enhancedSignInSecurity) + securityKeyForSignIn = ($Settings.securityKeyForSignIn.value ?? $CurrentState.securityKeyForSignIn) } | ConvertTo-Json -Compress -Depth 10 } try { From 1e02bfcb198406e9caa408acad12d79c7646ce6f Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Wed, 20 May 2026 23:42:04 -0400 Subject: [PATCH 017/202] feat(standards): add DLP via DCS standard --- .../Invoke-CIPPStandardDlpViaDcsEnabled.ps1 | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDlpViaDcsEnabled.ps1 diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDlpViaDcsEnabled.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDlpViaDcsEnabled.ps1 new file mode 100644 index 000000000000..4419a28bf8a0 --- /dev/null +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDlpViaDcsEnabled.ps1 @@ -0,0 +1,91 @@ +function Invoke-CIPPStandardDlpViaDcsEnabled { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) DlpViaDcsEnabled + .SYNOPSIS + (Label) Set OWA DLP evaluation via DCS + .DESCRIPTION + (Helptext) Sets whether Outlook on the web uses Data Classification Services for DLP evaluation. See [Microsoft's policy tip reference](https://learn.microsoft.com/en-us/purview/dlp-ol365-win32-policy-tips#sensitive-information-types-that-support-policy-tips-for-outlook-perpetual-users). + (DocsDescription) Configures whether Outlook on the web uses Data Classification Services (DCS)-based Data Loss Prevention (DLP) policy evaluation instead of Exchange-based evaluation. Review DLP policies before enabling this setting, as some legacy Exchange-based predicates are not supported with DCS-based evaluation. See [Microsoft's policy tip reference](https://learn.microsoft.com/en-us/purview/dlp-ol365-win32-policy-tips#sensitive-information-types-that-support-policy-tips-for-outlook-perpetual-users). + .NOTES + CAT + Exchange Standards + TAG + EXECUTIVETEXT + Improves how Outlook on the web applies Data Loss Prevention policies, giving users clearer guidance when sensitive information may be shared and helping reduce accidental data exposure. + ADDEDCOMPONENT + {"type":"autoComplete","multiple":false,"creatable":false,"label":"Select value","name":"standards.DlpViaDcsEnabled.state","options":[{"label":"Enabled","value":"true"},{"label":"Disabled","value":"false"}]} + IMPACT + Medium Impact + ADDEDDATE + 2026-05-20 + POWERSHELLEQUIVALENT + Set-OrganizationConfig -DlpViaDcsEnabled + RECOMMENDEDBY + REQUIREDCAPABILITIES + "EXCHANGE_S_STANDARD" + "EXCHANGE_S_ENTERPRISE" + "EXCHANGE_S_STANDARD_GOV" + "EXCHANGE_S_ENTERPRISE_GOV" + "EXCHANGE_LITE" + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + .LINK + https://docs.cipp.app/user-documentation/tenant/standards/alignment/templates/available-standards + #> + + param($Tenant, $Settings) + $TestResult = Test-CIPPStandardLicense -StandardName 'DlpViaDcsEnabled' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access + + if ($TestResult -eq $false) { + return $true + } #we're done. + + $state = $Settings.state.value ?? $Settings.state + if ([string]::IsNullOrWhiteSpace($state) -or $state -eq 'Select a value') { + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'DLP via Data Classification Service state not selected, skipping.' -sev Error + return + } + $WantedState = [System.Convert]::ToBoolean($state) + + try { + $CurrentInfo = (New-ExoRequest -tenantid $Tenant -cmdlet 'Get-OrganizationConfig').DlpViaDcsEnabled + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the DLP via Data Classification Service state for $Tenant. Error: $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage + return + } + + if ($Settings.remediate -eq $true) { + if ($CurrentInfo -ne $WantedState) { + try { + $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-OrganizationConfig' -cmdParams @{ DlpViaDcsEnabled = $WantedState } + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully set DLP via Data Classification Service to $state." -sev Info + $CurrentInfo = $WantedState + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to set DLP via Data Classification Service to $state. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + } + } else { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "DLP via Data Classification Service is already set to $state." -sev Info + } + } + + if ($Settings.alert -eq $true) { + if ($CurrentInfo -eq $WantedState) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "DLP via Data Classification Service is set to $state." -sev Info + } else { + Write-StandardsAlert -message "DLP via Data Classification Service is not set to $state" -object $CurrentInfo -tenant $Tenant -standardName 'DlpViaDcsEnabled' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $Tenant -message "DLP via Data Classification Service is not set to $state." -sev Info + } + } + + if ($Settings.report -eq $true) { + Add-CIPPBPAField -FieldName 'DlpViaDcsEnabled' -FieldValue $CurrentInfo -StoreAs bool -Tenant $Tenant + $CurrentValue = @{ DlpViaDcsEnabled = $CurrentInfo } + $ExpectedValue = @{ DlpViaDcsEnabled = $WantedState } + Set-CIPPStandardsCompareField -FieldName 'standards.DlpViaDcsEnabled' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant + } +} From cfa144d6b00b6641720c18bde34537227965a7fd Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Fri, 22 May 2026 12:51:10 -0400 Subject: [PATCH 018/202] Update Invoke-ListWorkerHealth.ps1 --- .../CIPP/Settings/Invoke-ListWorkerHealth.ps1 | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListWorkerHealth.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListWorkerHealth.ps1 index 66862547597b..f1753f41d93b 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListWorkerHealth.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListWorkerHealth.ps1 @@ -26,6 +26,24 @@ function Invoke-ListWorkerHealth { $Pool = [Craft.Services.WorkerMetricsBridge]::GetPoolMetrics($PoolType) $Body = @{ Results = $Pool } } + 'History' { + $Minutes = if ($Request.Query.Minutes) { [int]$Request.Query.Minutes } else { 60 } + $MaxPoints = if ($Request.Query.MaxPoints) { [int]$Request.Query.MaxPoints } else { $null } + $History = [Craft.Services.StatsHistoryBridge]::GetHistory($Minutes, $MaxPoints) + $Count = [Craft.Services.StatsHistoryBridge]::GetCount() + $Body = @{ + Results = @{ + TotalPoints = $Count + ReturnedPoints = $History.Count + RangeMinutes = $Minutes + Data = $History + } + } + } + 'Startup' { + $StartupInfo = [Craft.Services.StartupInfoBridge]::GetInfo() + $Body = @{ Results = $StartupInfo } + } 'Jobs' { $RunName = $Request.Query.RunName $Status = $Request.Query.Status From a89c2b9333ba7a1bca50fda53145894835de8f80 Mon Sep 17 00:00:00 2001 From: Luis Mengel Date: Sat, 23 May 2026 15:20:47 +0200 Subject: [PATCH 019/202] Add Group-Based Licensing support --- Modules/CIPPCore/Public/New-CIPPGroup.ps1 | 13 ++++ Modules/CIPPCore/Public/Remove-CIPPGroup.ps1 | 8 ++ .../CIPPCore/Public/Set-CIPPGroupLicense.ps1 | 75 +++++++++++++++++++ .../Groups/Invoke-AddGroupTemplate.ps1 | 3 +- .../Groups/Invoke-EditGroup.ps1 | 15 ++++ .../Groups/Invoke-ListGroupTemplates.ps1 | 1 + 6 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 Modules/CIPPCore/Public/Set-CIPPGroupLicense.ps1 diff --git a/Modules/CIPPCore/Public/New-CIPPGroup.ps1 b/Modules/CIPPCore/Public/New-CIPPGroup.ps1 index abf7db02e6ea..ff186140304d 100644 --- a/Modules/CIPPCore/Public/New-CIPPGroup.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPGroup.ps1 @@ -192,6 +192,19 @@ function New-CIPPGroup { } catch { Write-Warning "Failed to write group creation cache for $($GroupObject.displayName): $($_.Exception.Message)" } + + # Assign licenses for Security groups + if ($NormalizedGroupType -eq 'Generic' -and $GroupObject.licenses) { + $LicenseSkuIds = @($GroupObject.licenses | Where-Object { $_ } | ForEach-Object { $_.value ?? $_ }) + if ($LicenseSkuIds.Count -gt 0) { + try { + $null = Set-CIPPGroupLicense -GroupId $GraphRequest.id -GroupName $GroupObject.displayName -AddLicenses $LicenseSkuIds -TenantFilter $TenantFilter -APIName $APIName + } catch { + Write-Warning "Failed to assign licenses to group $($GroupObject.displayName): $($_.Exception.Message)" + } + } + } + if ($GroupObject.subscribeMembers) { #Waiting for group to become available in Exo. Start-Sleep -Seconds 10 diff --git a/Modules/CIPPCore/Public/Remove-CIPPGroup.ps1 b/Modules/CIPPCore/Public/Remove-CIPPGroup.ps1 index 3f1bdafec13a..3fda76c25b58 100644 --- a/Modules/CIPPCore/Public/Remove-CIPPGroup.ps1 +++ b/Modules/CIPPCore/Public/Remove-CIPPGroup.ps1 @@ -16,6 +16,14 @@ function Remove-CIPPGroup { return "Successfully Deleted $($GroupType) group $($DisplayName)" } elseif ($GroupType -eq 'Microsoft 365' -or $GroupType -eq 'Security') { + # Remove any assigned licenses + if ($GroupType -eq 'Security') { + $Group = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/groups/$($ID)?`$select=assignedLicenses" -tenantid $TenantFilter + $CurrentSkus = @($Group.assignedLicenses.skuId | Where-Object { $_ }) + if ($CurrentSkus.Count -gt 0) { + $null = Set-CIPPGroupLicense -GroupId $ID -GroupName $DisplayName -RemoveLicenses $CurrentSkus -TenantFilter $TenantFilter -Headers $Headers -APIName $APIName + } + } $null = New-GraphPostRequest -uri "https://graph.microsoft.com/v1.0/groups/$($ID)" -tenantid $TenantFilter -type Delete -verbose Write-LogMessage -headers $Headers -API $APINAME -tenant $($TenantFilter) -message "$($DisplayName) Deleted" -Sev 'Info' return "Successfully Deleted $($GroupType) group $($DisplayName)" diff --git a/Modules/CIPPCore/Public/Set-CIPPGroupLicense.ps1 b/Modules/CIPPCore/Public/Set-CIPPGroupLicense.ps1 new file mode 100644 index 000000000000..fb31cc7eccb9 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPGroupLicense.ps1 @@ -0,0 +1,75 @@ +function Set-CIPPGroupLicense { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)][string]$GroupId, + [string]$GroupName, + [array]$AddLicenses = @(), + [array]$RemoveLicenses = @(), + [switch]$Replace, + [Parameter(Mandatory = $true)][string]$TenantFilter, + $Headers, + $APIName = 'Set Group License' + ) + + $AddLicenses = @( + $AddLicenses | + ForEach-Object { [string]$_ } | + Where-Object { -not [string]::IsNullOrWhiteSpace($_) } + ) + $RemoveLicenses = @( + $RemoveLicenses | + ForEach-Object { [string]$_ } | + Where-Object { -not [string]::IsNullOrWhiteSpace($_) } + ) + + if ( [string]::IsNullOrWhiteSpace($GroupName)) { + $GroupName = $GroupId + } + + # fetch current assigned licenses, calculate the diff and replace with licenses + if ($Replace.IsPresent) { + try { + $Current = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/groups/$($GroupId)?`$select=assignedLicenses" -tenantid $TenantFilter + $CurrentSkus = @($Current.assignedLicenses.skuId) + $RemoveLicenses = @($CurrentSkus | Where-Object { $_ -notin $AddLicenses }) + $AddLicenses = @($AddLicenses | Where-Object { $_ -notin $CurrentSkus }) + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API $APIName -tenant $TenantFilter -Headers $Headers -message "Failed to fetch current licenses for group $GroupName. Error: $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage + throw "Failed to fetch current licenses for group $GroupName. Error: $($ErrorMessage.NormalizedError)" + } + } + + if ($AddLicenses.Count -eq 0 -and $RemoveLicenses.Count -eq 0) { + return @("No license changes required for group $GroupName.") + } + + $AddLicensesArray = foreach ($SkuId in $AddLicenses) { + @{ disabledPlans = @(); skuId = $SkuId } + } + $LicenseBody = @{ + addLicenses = @($AddLicensesArray) + removeLicenses = @($RemoveLicenses) + } | ConvertTo-Json -Compress -Depth 10 + + $Results = [System.Collections.Generic.List[string]]::new() + try { + $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/groups/$($GroupId)/assignLicense" -tenantid $TenantFilter -body $LicenseBody -type POST + + if ($AddLicenses.Count -gt 0) { + $Message = "Assigned licenses to group $GroupName. Added: $($AddLicenses -join ', ')" + [void]$Results.Add("$Message. It may take 2-5 minutes for changes to apply to members.") + Write-LogMessage -API $APIName -tenant $TenantFilter -Headers $Headers -message $Message -Sev Info + } + if ($RemoveLicenses.Count -gt 0) { + $Message = "Removed licenses from group $GroupName. Removed: $($RemoveLicenses -join ', ')" + [void]$Results.Add("$Message. It may take 2-5 minutes for changes to apply to members.") + Write-LogMessage -API $APIName -tenant $TenantFilter -Headers $Headers -message $Message -Sev Info + } + return $Results + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API $APIName -tenant $TenantFilter -Headers $Headers -message "Failed to update licenses for group $GroupName. Error: $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage + throw "Failed to update licenses for group $GroupName. Error: $($ErrorMessage.NormalizedError)" + } +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-AddGroupTemplate.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-AddGroupTemplate.ps1 index 5f709fd9db24..8d17d0aa0118 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-AddGroupTemplate.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-AddGroupTemplate.ps1 @@ -52,8 +52,9 @@ function Invoke-AddGroupTemplate { membershipRules = $MembershipRules allowExternal = $Request.Body.allowExternal username = $Request.Body.username # Can contain variables like @%tenantfilter% + licenses = $Request.Body.licenses GUID = $GUID - } | ConvertTo-Json + } | ConvertTo-Json -Depth 10 $Table = Get-CippTable -tablename 'templates' $Table.Force = $true Add-CIPPAzDataTableEntity @Table -Force -Entity @{ diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-EditGroup.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-EditGroup.ps1 index a94233ce1e8c..3d4ee59f05e2 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-EditGroup.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-EditGroup.ps1 @@ -355,6 +355,21 @@ function Invoke-EditGroup { } } + # Only process licenses for Security groups + if ($GroupType -eq 'Security') { + $AddLicenses = @($UserObj.AddLicenses | Where-Object { $_ } | ForEach-Object { $_.value ?? $_ }) + $RemoveLicenses = @($UserObj.RemoveLicenses | Where-Object { $_ } | ForEach-Object { $_.value ?? $_ }) + + if ($AddLicenses.Count -gt 0 -or $RemoveLicenses.Count -gt 0) { + try { + $LicResults = Set-CIPPGroupLicense -GroupId $GroupId -GroupName $GroupName -AddLicenses $AddLicenses -RemoveLicenses $RemoveLicenses -TenantFilter $TenantId -Headers $Headers -APIName $APIName + foreach ($LicResult in $LicResults) { $Results.Add("Success - $LicResult") } + } catch { + $Results.Add("Error - $($_.Exception.Message)") + } + } + } + # Only process allowExternal if it was explicitly sent if ($null -ne $UserObj.allowExternal -and $GroupType -ne 'Security') { try { diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroupTemplates.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroupTemplates.ps1 index b58a93aa3ced..4a4373733049 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroupTemplates.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroupTemplates.ps1 @@ -47,6 +47,7 @@ function Invoke-ListGroupTemplates { membershipRules = $data.membershipRules allowExternal = $data.allowExternal username = $data.username + licenses = $data.licenses GUID = $_.RowKey source = $_.Source isSynced = (![string]::IsNullOrEmpty($_.SHA)) From 77a4be6499e8e93d41a932983f03799275071893 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sat, 23 May 2026 18:10:15 +0200 Subject: [PATCH 020/202] fixes #6027 --- .../HTTP Functions/CIPP/Settings/Invoke-ExecTenantGroup.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecTenantGroup.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecTenantGroup.ps1 index a5e20fc7125e..a0d869ade67f 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecTenantGroup.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecTenantGroup.ps1 @@ -112,7 +112,7 @@ function Invoke-ExecTenantGroup { $Adds.Add('Added member {0}' -f $member.label) } - if ($CurrentMembers -and $members) { + if ($CurrentMembers -and $null -ne $members) { foreach ($CurrentMember in $CurrentMembers) { if ($members.value -notcontains $CurrentMember.customerId) { Remove-AzDataTableEntity @MembersTable -Entity $CurrentMember -Force From 4ab85c751fe6aad0c93d41ce43eb1e7755ae12c4 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Sun, 24 May 2026 09:13:56 +1000 Subject: [PATCH 021/202] CIPP Hosted Notices --- .../CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 | 8 ++++++++ .../Entrypoints/HTTP Functions/New-CippCoreRequest.ps1 | 9 +++++++++ 2 files changed, 17 insertions(+) diff --git a/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 b/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 index 9622bf5f0c9e..af0d8bb74bf6 100644 --- a/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 +++ b/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 @@ -209,6 +209,14 @@ function Test-CIPPAccess { 'permissions' = $Permissions } + # Hosted payment status checks — shown to all users (no permission gating) + if ($env:cipp_hosted_subscription_ended) { + $MeResponse['hostedSubscriptionEnded'] = $true + } + if ($env:cipp_hosted_failed_payments) { + $MeResponse['hostedFailedPayments'] = $true + } + # Forced SSO migration: non-dismissible prompt when migration env var is set if ($env:CIPP_SSO_MIGRATION_APPID -and $Permissions -contains 'CIPP.AppSettings.ReadWrite') { $MeResponse['forceSsoMigration'] = @{ diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/New-CippCoreRequest.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/New-CippCoreRequest.ps1 index 87fc325b6c1e..a457080639d2 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/New-CippCoreRequest.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/New-CippCoreRequest.ps1 @@ -58,6 +58,15 @@ function New-CippCoreRequest { }) } + # Block all API calls except /api/me when subscription has ended + if ($env:cipp_hosted_subscription_ended -and $Request.Params.CIPPEndpoint -ne 'me') { + $HttpTotalStopwatch.Stop() + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::Forbidden + Body = 'Your CIPP subscription has ended. Access to this instance is no longer available.' + }) + } + if ($Request.Headers.'X-CIPP-Version') { $Table = Get-CippTable -tablename 'Version' $FrontendVer = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq 'Version' and RowKey eq 'frontend'" From dcf382aaa03539256b17454a49371ac7fda6485e Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Sun, 24 May 2026 09:33:25 +1000 Subject: [PATCH 022/202] Update Build-DevApiModules.ps1 --- Tools/Build-DevApiModules.ps1 | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Tools/Build-DevApiModules.ps1 b/Tools/Build-DevApiModules.ps1 index c11524f4724b..5510a3a36e83 100644 --- a/Tools/Build-DevApiModules.ps1 +++ b/Tools/Build-DevApiModules.ps1 @@ -5,9 +5,7 @@ $repoRoot = Split-Path -Parent $toolsRoot $modulesRoot = Join-Path $repoRoot 'Modules' $outputRoot = Join-Path $repoRoot 'Output' -if (-not (Get-Module -ListAvailable -Name ModuleBuilder)) { - Install-Module -Name ModuleBuilder -Scope CurrentUser -Force -} +Install-Module -Name ModuleBuilder -Scope CurrentUser -Force -AllowClobber Import-Module -Name ModuleBuilder -Force Write-Host "Repo root: $repoRoot" From 9bb2f6bc962c1fbb89801b6e50295407672ca829 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Sun, 24 May 2026 09:38:26 +1000 Subject: [PATCH 023/202] Update Build-DevApiModules.ps1 --- Tools/Build-DevApiModules.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tools/Build-DevApiModules.ps1 b/Tools/Build-DevApiModules.ps1 index 5510a3a36e83..04b94fbe195c 100644 --- a/Tools/Build-DevApiModules.ps1 +++ b/Tools/Build-DevApiModules.ps1 @@ -5,7 +5,7 @@ $repoRoot = Split-Path -Parent $toolsRoot $modulesRoot = Join-Path $repoRoot 'Modules' $outputRoot = Join-Path $repoRoot 'Output' -Install-Module -Name ModuleBuilder -Scope CurrentUser -Force -AllowClobber +Install-Module -Name ModuleBuilder -MaximumVersion 3.1.9 -Scope CurrentUser -Force -AllowClobber Import-Module -Name ModuleBuilder -Force Write-Host "Repo root: $repoRoot" From fa5f4de6f54e7efc2473687e705d5b6caf20b1aa Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sun, 24 May 2026 14:46:53 -0400 Subject: [PATCH 024/202] remove sso setup from featureflag --- Config/FeatureFlags.json | 1 - 1 file changed, 1 deletion(-) diff --git a/Config/FeatureFlags.json b/Config/FeatureFlags.json index 35c51a44e865..1216ac071689 100644 --- a/Config/FeatureFlags.json +++ b/Config/FeatureFlags.json @@ -32,7 +32,6 @@ "Endpoints": [ "ExecCIPPUsers", "ListCIPPUsers", - "ExecSSOSetup", "ExecContainerManagement", "ListContainerLogs", "ListWorkerHealth" From df8477792bb4970416689b732c43a95e3537be7b Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 24 May 2026 22:41:08 +0200 Subject: [PATCH 025/202] implement standards template deployment for intune apps --- .../Applications/Push-UploadApplication.ps1 | 139 +----------- .../Public/New-CIPPIntuneAppDeployment.ps1 | 204 ++++++++++++++++++ ...ke-CIPPStandardIntuneAppTemplateDeploy.ps1 | 142 ++++++++++++ 3 files changed, 347 insertions(+), 138 deletions(-) create mode 100644 Modules/CIPPCore/Public/New-CIPPIntuneAppDeployment.ps1 create mode 100644 Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardIntuneAppTemplateDeploy.ps1 diff --git a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Applications/Push-UploadApplication.ps1 b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Applications/Push-UploadApplication.ps1 index ba5d57d4a1a7..914801bcb29d 100644 --- a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Applications/Push-UploadApplication.ps1 +++ b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Applications/Push-UploadApplication.ps1 @@ -11,15 +11,11 @@ function Push-UploadApplication { $Filter = "PartitionKey eq 'apps' and RowKey eq '$($Item.Name)'" $AppConfig = (Get-CIPPAzDataTableEntity @Table -filter $Filter).JSON | ConvertFrom-Json - $intuneBody = $AppConfig.IntuneBody $tenants = if ($AppConfig.tenant -eq 'AllTenants') { (Get-Tenants -IncludeErrors).defaultDomainName } else { $AppConfig.tenant } - $assignTo = $AppConfig.assignTo - $AssignToIntent = $AppConfig.InstallationIntent - $ExcludeGroup = $AppConfig.excludeGroup $ClearRow = Get-CIPPAzDataTableEntity @Table -Filter $Filter if ($AppConfig.tenant -ne 'AllTenants') { $null = Remove-AzDataTableEntity -Force @Table -Entity $clearRow @@ -33,142 +29,9 @@ function Push-UploadApplication { } } - # Determine app type (default to 'Choco' if not specified) - $AppType = if ($AppConfig.type) { $AppConfig.type } else { 'Choco' } - - # Load files based on app type (only for types that need them) - $Intunexml = $null - $Infile = $null - if ($AppType -eq 'MSPApp') { - [xml]$Intunexml = Get-Content (Join-Path $env:CIPPRootPath "AddMSPApp\$($AppConfig.MSPAppName).app.xml") - $Infile = Join-Path $env:CIPPRootPath "AddMSPApp\$($AppConfig.MSPAppName).intunewin" - } elseif ($AppType -in @('Choco', 'Win32ScriptApp')) { - [xml]$Intunexml = Get-Content (Join-Path $env:CIPPRootPath 'AddChocoApp\Choco.App.xml') - $Infile = Join-Path $env:CIPPRootPath "AddChocoApp\$($Intunexml.ApplicationInfo.FileName)" - } - - - $baseuri = 'https://graph.microsoft.com/beta/deviceAppManagement/mobileApps' foreach ($tenant in $tenants) { try { - # Check if app already exists - $ApplicationList = New-GraphGetRequest -Uri $baseuri -tenantid $tenant | Where-Object { $_.DisplayName -eq $AppConfig.Applicationname -and ($_.'@odata.type' -eq '#microsoft.graph.win32LobApp' -or $_.'@odata.type' -eq '#microsoft.graph.winGetApp') } - if ($ApplicationList.displayname.count -ge 1) { - Write-LogMessage -api 'AppUpload' -tenant $tenant -message "$($AppConfig.Applicationname) exists. Skipping this application" -Sev 'Info' - continue - } - - # Route to appropriate handler based on app type - $NewApp = $null - switch ($AppType) { - 'WinGet' { - $NewApp = Add-CIPPWinGetApp -AppBody $intuneBody -TenantFilter $tenant - } - 'Choco' { - # Prepare encryption info from XML - $EncryptionInfo = @{ - EncryptionKey = $Intunexml.ApplicationInfo.EncryptionInfo.EncryptionKey - MacKey = $Intunexml.ApplicationInfo.EncryptionInfo.MacKey - InitializationVector = $Intunexml.ApplicationInfo.EncryptionInfo.InitializationVector - Mac = $Intunexml.ApplicationInfo.EncryptionInfo.Mac - ProfileIdentifier = $Intunexml.ApplicationInfo.EncryptionInfo.ProfileIdentifier - FileDigest = $Intunexml.ApplicationInfo.EncryptionInfo.FileDigest - FileDigestAlgorithm = $Intunexml.ApplicationInfo.EncryptionInfo.FileDigestAlgorithm - } - - # Build parameters dynamically - $Params = @{ - AppBody = $intuneBody - TenantFilter = $tenant - FilePath = $Infile - FileName = $Intunexml.ApplicationInfo.FileName - UnencryptedSize = [int64]$Intunexml.ApplicationInfo.UnencryptedContentSize - EncryptionInfo = $EncryptionInfo - } - if ($AppConfig.Applicationname) { $Params.DisplayName = $AppConfig.Applicationname } - - $NewApp = Add-CIPPPackagedApplication @Params - } - 'MSPApp' { - # Prepare encryption info from XML - $EncryptionInfo = @{ - EncryptionKey = $Intunexml.ApplicationInfo.EncryptionInfo.EncryptionKey - MacKey = $Intunexml.ApplicationInfo.EncryptionInfo.MacKey - InitializationVector = $Intunexml.ApplicationInfo.EncryptionInfo.InitializationVector - Mac = $Intunexml.ApplicationInfo.EncryptionInfo.Mac - ProfileIdentifier = $Intunexml.ApplicationInfo.EncryptionInfo.ProfileIdentifier - FileDigest = $Intunexml.ApplicationInfo.EncryptionInfo.FileDigest - FileDigestAlgorithm = $Intunexml.ApplicationInfo.EncryptionInfo.FileDigestAlgorithm - } - - # Build parameters dynamically - $Params = @{ - AppBody = $intuneBody - TenantFilter = $tenant - FilePath = $Infile - FileName = $Intunexml.ApplicationInfo.FileName - UnencryptedSize = [int64]$Intunexml.ApplicationInfo.UnencryptedContentSize - EncryptionInfo = $EncryptionInfo - } - if ($AppConfig.Applicationname) { $Params.DisplayName = $AppConfig.Applicationname } - - $NewApp = Add-CIPPPackagedApplication @Params - } - 'Win32ScriptApp' { - # Prepare encryption info from XML - $EncryptionInfo = @{ - EncryptionKey = $Intunexml.ApplicationInfo.EncryptionInfo.EncryptionKey - MacKey = $Intunexml.ApplicationInfo.EncryptionInfo.MacKey - InitializationVector = $Intunexml.ApplicationInfo.EncryptionInfo.InitializationVector - Mac = $Intunexml.ApplicationInfo.EncryptionInfo.Mac - ProfileIdentifier = $Intunexml.ApplicationInfo.EncryptionInfo.ProfileIdentifier - FileDigest = $Intunexml.ApplicationInfo.EncryptionInfo.FileDigest - FileDigestAlgorithm = $Intunexml.ApplicationInfo.EncryptionInfo.FileDigestAlgorithm - } - - # Build properties dynamically - $Properties = @{ - displayName = $AppConfig.Applicationname - installScript = $AppConfig.installScript - } - - # A few of these are probably mandatory - if ($AppConfig.description) { $Properties['description'] = $AppConfig.description } - if ($AppConfig.publisher) { $Properties['publisher'] = $AppConfig.publisher } - if ($AppConfig.uninstallScript) { $Properties['uninstallScript'] = $AppConfig.uninstallScript } - if ($AppConfig.detectionScript) { $Properties['detectionScript'] = $AppConfig.detectionScript } - if ($AppConfig.detectionPath) { $Properties['detectionPath'] = $AppConfig.detectionPath } - if ($AppConfig.detectionFile) { $Properties['detectionFile'] = $AppConfig.detectionFile } - if ($AppConfig.runAsAccount) { $Properties['runAsAccount'] = $AppConfig.runAsAccount } - if ($AppConfig.deviceRestartBehavior) { $Properties['deviceRestartBehavior'] = $AppConfig.deviceRestartBehavior } - if ($null -ne $AppConfig.runAs32Bit) { $Properties['runAs32Bit'] = $AppConfig.runAs32Bit } - if ($null -ne $AppConfig.enforceSignatureCheck) { $Properties['enforceSignatureCheck'] = $AppConfig.enforceSignatureCheck } - - $NewApp = Add-CIPPW32ScriptApplication -TenantFilter $tenant -Properties ([PSCustomObject]$Properties) - } - 'WinGetNew' { - # I think we don't need a separate WinGetNew type, just use WinGet? - } - default { - throw "Unsupported app type: $($AppConfig.type)" - } - } - - # Log success and assign app if requested - if ($NewApp) { - Write-LogMessage -api 'AppUpload' -tenant $tenant -message "$($AppConfig.Applicationname) Successfully created" -Sev 'Info' - - if ($assignTo -and $assignTo -ne 'On') { - $intent = if ($AssignToIntent) { 'Uninstall' } else { 'Required' } - $AppTypeForAssignment = switch ($AppType) { - 'WinGet' { 'WinGet' } - 'WinGetNew' { 'WinGet' } - default { 'Win32Lob' } - } - Start-Sleep -Milliseconds 200 - Set-CIPPAssignedApplication -ApplicationId $NewApp.Id -TenantFilter $tenant -groupName $assignTo -ExcludeGroup $ExcludeGroup -Intent $intent -AppType $AppTypeForAssignment -APIName 'AppUpload' - } - } + $NewApp = New-CIPPIntuneAppDeployment -AppConfig $AppConfig -TenantFilter $tenant -APIName 'AppUpload' } catch { "Failed to add Application for $tenant : $($_.Exception.Message)" Write-LogMessage -api 'AppUpload' -tenant $tenant -message "Failed adding Application $($AppConfig.Applicationname). Error: $($_.Exception.Message)" -LogData (Get-CippException -Exception $_) -Sev 'Error' diff --git a/Modules/CIPPCore/Public/New-CIPPIntuneAppDeployment.ps1 b/Modules/CIPPCore/Public/New-CIPPIntuneAppDeployment.ps1 new file mode 100644 index 000000000000..be7ca21b9787 --- /dev/null +++ b/Modules/CIPPCore/Public/New-CIPPIntuneAppDeployment.ps1 @@ -0,0 +1,204 @@ +function New-CIPPIntuneAppDeployment { + <# + .SYNOPSIS + Deploys a single Intune application to a tenant. + .DESCRIPTION + Shared deployment function used by both Push-UploadApplication (queue processing) + and standards. Handles app existence check, type routing, and assignment. + Accepts either a pre-built AppConfig (with IntuneBody, from queue) or raw template + config (appType + raw fields) and builds the deployment config internally. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [PSCustomObject]$AppConfig, + + [Parameter(Mandatory = $true)] + [string]$TenantFilter, + + [string]$APIName = 'AppUpload' + ) + + $IntuneBody = $AppConfig.IntuneBody + $AssignTo = $AppConfig.assignTo + $AssignToIntent = $AppConfig.InstallationIntent + $ExcludeGroup = $AppConfig.excludeGroup + $AppType = if ($AppConfig.type) { $AppConfig.type } else { 'Choco' } + + # Build IntuneBody from raw config if not pre-built (template/standard path) + if (-not $IntuneBody -and $AppType -eq 'WinGet') { + $PackageId = $AppConfig.packagename ?? $AppConfig.PackageName + $AppDisplayName = $AppConfig.applicationName ?? $AppConfig.ApplicationName + if (-not $PackageId) { + throw "PackageName/packagename is required for WinGet apps but was not found in the config for '$AppDisplayName'." + } + $IntuneBody = [ordered]@{ + '@odata.type' = '#microsoft.graph.winGetApp' + 'displayName' = "$AppDisplayName" + 'description' = "$($AppConfig.description)" + 'packageIdentifier' = "$PackageId" + 'installExperience' = @{ + '@odata.type' = 'microsoft.graph.winGetAppInstallExperience' + 'runAsAccount' = 'system' + } + } + } + + if (-not $IntuneBody -and $AppType -eq 'Choco') { + $IntuneBody = Get-Content (Join-Path $env:CIPPRootPath 'AddChocoApp\Choco.app.json') | ConvertFrom-Json + $IntuneBody.description = $AppConfig.description + $IntuneBody.displayName = $AppConfig.ApplicationName + $IntuneBody.installExperience.runAsAccount = if ($AppConfig.InstallAsSystem) { 'system' } else { 'user' } + $IntuneBody.installExperience.deviceRestartBehavior = if ($AppConfig.DisableRestart) { 'suppress' } else { 'allow' } + $IntuneBody.installCommandLine = "powershell.exe -ExecutionPolicy Bypass .\Install.ps1 -InstallChoco -Packagename $($AppConfig.PackageName)" + if ($AppConfig.customrepo) { + $IntuneBody.installCommandLine = $IntuneBody.installCommandLine + " -CustomRepo $($AppConfig.CustomRepo)" + } + if ($AppConfig.customArguments) { + $IntuneBody.installCommandLine = $IntuneBody.installCommandLine + " -CustomArguments '$($AppConfig.customArguments)'" + } + $IntuneBody.UninstallCommandLine = "powershell.exe -ExecutionPolicy Bypass .\Uninstall.ps1 -Packagename $($AppConfig.PackageName)" + $IntuneBody.detectionRules[0].path = "$($ENV:SystemDrive)\programdata\chocolatey\lib" + $IntuneBody.detectionRules[0].fileOrFolderName = "$($AppConfig.PackageName)" + + if ($IntuneBody.installCommandLine -match '%') { + $IntuneBody.installCommandLine = Get-CIPPTextReplacement -TenantFilter $TenantFilter -Text $IntuneBody.installCommandLine + } + } + + # Load files based on app type (only for types that need them) + $Intunexml = $null + $Infile = $null + if ($AppType -eq 'MSPApp') { + [xml]$Intunexml = Get-Content (Join-Path $env:CIPPRootPath "AddMSPApp\$($AppConfig.MSPAppName).app.xml") + $Infile = Join-Path $env:CIPPRootPath "AddMSPApp\$($AppConfig.MSPAppName).intunewin" + } elseif ($AppType -in @('Choco', 'Win32ScriptApp')) { + [xml]$Intunexml = Get-Content (Join-Path $env:CIPPRootPath 'AddChocoApp\Choco.App.xml') + $Infile = Join-Path $env:CIPPRootPath "AddChocoApp\$($Intunexml.ApplicationInfo.FileName)" + } + + $BaseUri = 'https://graph.microsoft.com/beta/deviceAppManagement/mobileApps' + + # Check if app already exists (any type with matching display name) + $ApplicationList = New-GraphGetRequest -Uri $BaseUri -tenantid $TenantFilter | Where-Object { $_.DisplayName -eq $AppConfig.Applicationname } + if ($ApplicationList.displayname.count -ge 1) { + Write-LogMessage -API $APIName -tenant $TenantFilter -message "$($AppConfig.Applicationname) exists. Skipping this application" -Sev 'Info' + return $null + } + + # Route to appropriate handler based on app type + $NewApp = $null + switch ($AppType) { + 'WinGet' { + $NewApp = Add-CIPPWinGetApp -AppBody $IntuneBody -TenantFilter $TenantFilter + } + 'Choco' { + $EncryptionInfo = @{ + EncryptionKey = $Intunexml.ApplicationInfo.EncryptionInfo.EncryptionKey + MacKey = $Intunexml.ApplicationInfo.EncryptionInfo.MacKey + InitializationVector = $Intunexml.ApplicationInfo.EncryptionInfo.InitializationVector + Mac = $Intunexml.ApplicationInfo.EncryptionInfo.Mac + ProfileIdentifier = $Intunexml.ApplicationInfo.EncryptionInfo.ProfileIdentifier + FileDigest = $Intunexml.ApplicationInfo.EncryptionInfo.FileDigest + FileDigestAlgorithm = $Intunexml.ApplicationInfo.EncryptionInfo.FileDigestAlgorithm + } + + $Params = @{ + AppBody = $IntuneBody + TenantFilter = $TenantFilter + FilePath = $Infile + FileName = $Intunexml.ApplicationInfo.FileName + UnencryptedSize = [int64]$Intunexml.ApplicationInfo.UnencryptedContentSize + EncryptionInfo = $EncryptionInfo + } + if ($AppConfig.Applicationname) { $Params.DisplayName = $AppConfig.Applicationname } + + $NewApp = Add-CIPPPackagedApplication @Params + } + 'MSPApp' { + $EncryptionInfo = @{ + EncryptionKey = $Intunexml.ApplicationInfo.EncryptionInfo.EncryptionKey + MacKey = $Intunexml.ApplicationInfo.EncryptionInfo.MacKey + InitializationVector = $Intunexml.ApplicationInfo.EncryptionInfo.InitializationVector + Mac = $Intunexml.ApplicationInfo.EncryptionInfo.Mac + ProfileIdentifier = $Intunexml.ApplicationInfo.EncryptionInfo.ProfileIdentifier + FileDigest = $Intunexml.ApplicationInfo.EncryptionInfo.FileDigest + FileDigestAlgorithm = $Intunexml.ApplicationInfo.EncryptionInfo.FileDigestAlgorithm + } + + $Params = @{ + AppBody = $IntuneBody + TenantFilter = $TenantFilter + FilePath = $Infile + FileName = $Intunexml.ApplicationInfo.FileName + UnencryptedSize = [int64]$Intunexml.ApplicationInfo.UnencryptedContentSize + EncryptionInfo = $EncryptionInfo + } + if ($AppConfig.Applicationname) { $Params.DisplayName = $AppConfig.Applicationname } + + $NewApp = Add-CIPPPackagedApplication @Params + } + 'Win32ScriptApp' { + $Properties = @{ + displayName = $AppConfig.Applicationname + installScript = $AppConfig.installScript + } + + if ($AppConfig.description) { $Properties['description'] = $AppConfig.description } + if ($AppConfig.publisher) { $Properties['publisher'] = $AppConfig.publisher } + if ($AppConfig.uninstallScript) { $Properties['uninstallScript'] = $AppConfig.uninstallScript } + if ($AppConfig.detectionScript) { $Properties['detectionScript'] = $AppConfig.detectionScript } + if ($AppConfig.detectionPath) { $Properties['detectionPath'] = $AppConfig.detectionPath } + if ($AppConfig.detectionFile) { $Properties['detectionFile'] = $AppConfig.detectionFile } + if ($AppConfig.runAsAccount) { $Properties['runAsAccount'] = $AppConfig.runAsAccount } + if ($AppConfig.deviceRestartBehavior) { $Properties['deviceRestartBehavior'] = $AppConfig.deviceRestartBehavior } + if ($null -ne $AppConfig.runAs32Bit) { $Properties['runAs32Bit'] = $AppConfig.runAs32Bit } + if ($null -ne $AppConfig.enforceSignatureCheck) { $Properties['enforceSignatureCheck'] = $AppConfig.enforceSignatureCheck } + + $NewApp = Add-CIPPW32ScriptApplication -TenantFilter $TenantFilter -Properties ([PSCustomObject]$Properties) + } + 'OfficeApp' { + # Strip read-only properties that Graph API won't accept on create + $ObjBody = $IntuneBody + if ($ObjBody -is [string]) { $ObjBody = $ObjBody | ConvertFrom-Json -Depth 100 } + $ReadOnlyProps = @('id', 'createdDateTime', 'lastModifiedDateTime', 'uploadState', 'publishingState', 'isAssigned', 'roleScopeTagIds', 'dependentAppCount', 'supersedingAppCount', 'supersededAppCount', 'committedContentVersion', 'fileName', 'size', 'assignments@odata.context', 'assignments', 'AppAssignment', 'AppExclude') + foreach ($prop in $ReadOnlyProps) { + if ($ObjBody.PSObject.Properties[$prop]) { + $ObjBody.PSObject.Properties.Remove($prop) + } + } + $NewApp = New-GraphPostRequest -Uri $BaseUri -tenantid $TenantFilter -Body (ConvertTo-Json -InputObject $ObjBody -Depth 10) -Type POST + } + default { + throw "Unsupported app type: $AppType" + } + } + + # Log success and assign app if requested + if ($NewApp) { + Write-LogMessage -API $APIName -tenant $TenantFilter -message "$($AppConfig.Applicationname) Successfully created" -Sev 'Info' + + if ($AssignTo -and $AssignTo -ne 'On') { + $Intent = if ($AssignToIntent) { 'Uninstall' } else { 'Required' } + $AppTypeForAssignment = switch ($AppType) { + 'WinGet' { 'WinGet' } + 'WinGetNew' { 'WinGet' } + 'OfficeApp' { $null } + default { 'Win32Lob' } + } + Start-Sleep -Milliseconds 200 + $AssignParams = @{ + ApplicationId = $NewApp.Id + TenantFilter = $TenantFilter + GroupName = $AssignTo + ExcludeGroup = $ExcludeGroup + Intent = $Intent + APIName = $APIName + } + if ($AppTypeForAssignment) { $AssignParams.AppType = $AppTypeForAssignment } + Set-CIPPAssignedApplication @AssignParams + } + } + + return $NewApp +} diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardIntuneAppTemplateDeploy.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardIntuneAppTemplateDeploy.ps1 new file mode 100644 index 000000000000..cc1b5013d9e5 --- /dev/null +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardIntuneAppTemplateDeploy.ps1 @@ -0,0 +1,142 @@ +function Invoke-CIPPStandardIntuneAppTemplateDeploy { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) IntuneAppTemplateDeploy + .SYNOPSIS + (Label) Deploy Intune Application Template + .DESCRIPTION + (Helptext) Deploys selected Intune application templates to the tenant. Supports WinGet/Store apps, Office apps, Chocolatey apps, Win32 script apps, and MSP apps. + (DocsDescription) Uses CIPP Intune Application Templates to deploy applications across tenants as a standard. Each template can contain multiple applications of different types which will be queued for deployment. + .NOTES + CAT + Intune Standards + TAG + EXECUTIVETEXT + Automatically deploys approved Intune applications across all managed tenants, ensuring consistent software availability and reducing manual deployment overhead. Supports WinGet, Office, Chocolatey, Win32, and MSP application types. + ADDEDCOMPONENT + {"type":"autoComplete","multiple":true,"creatable":false,"label":"Select Application Templates","name":"standards.IntuneAppTemplateDeploy.templateIds","api":{"url":"/api/ListAppTemplates","labelField":"Displayname","valueField":"GUID","queryKey":"StdIntuneAppTemplateList"}} + IMPACT + Medium Impact + ADDEDDATE + 2026-05-23 + POWERSHELLEQUIVALENT + Graph API - /deviceAppManagement/mobileApps + RECOMMENDEDBY + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + .LINK + https://docs.cipp.app/user-documentation/tenant/standards/alignment/templates/available-standards + #> + + param( + $Tenant, + $Settings + ) + + $TestResult = Test-CIPPStandardLicense -StandardName 'IntuneAppTemplateDeploy' -TenantFilter $Tenant -Preset Intune + if ($TestResult -eq $false) { return $true } + + $TemplateIds = @($Settings.templateIds.value ?? $Settings.templateIds) + if ($TemplateIds.Count -eq 0) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'IntuneAppTemplateDeploy: No template IDs provided, skipping.' -sev Error + return + } + + # Get current Intune apps via live Graph call (same as Push-UploadApplication) + $BaseUri = 'https://graph.microsoft.com/beta/deviceAppManagement/mobileApps' + $CurrentApps = @(New-GraphGetRequest -Uri $BaseUri -tenantid $Tenant) + + # Load all selected templates and build per-app objects + $Table = Get-CIPPTable -TableName 'templates' + $MissingApps = [System.Collections.Generic.List[PSCustomObject]]::new() + $CurrentAppNames = @($CurrentApps.displayName) + + foreach ($TemplateId in $TemplateIds) { + $Entity = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq 'AppTemplate' and RowKey eq '$TemplateId'" + if (-not $Entity) { continue } + + $TemplateData = $Entity.JSON | ConvertFrom-Json -Depth 100 + $TemplateName = $TemplateData.Displayname + $AppsRaw = $TemplateData.Apps + + # Build individual app objects from the template apps collection + $AppTypes = @($AppsRaw.appType) + $AppNames = @($AppsRaw.appName) + $AppConfigs = @($AppsRaw.config) + + for ($i = 0; $i -lt $AppTypes.Count; $i++) { + $RawConfig = $AppConfigs[$i] + $Config = if ($RawConfig -is [string]) { $RawConfig | ConvertFrom-Json -Depth 100 } else { $RawConfig } + $DisplayName = [string]($Config.ApplicationName ?? $Config.displayName ?? $AppNames[$i]) + + if ($DisplayName -notin $CurrentAppNames) { + $MissingApps.Add([PSCustomObject]@{ + TemplateId = [string]$TemplateId + TemplateName = [string]$TemplateName + AppName = [string]$DisplayName + AppType = [string]$AppTypes[$i] + Config = $Config + }) + } + } + } + + $ExpectedValue = [PSCustomObject]@{ state = 'All template apps deployed' } + $CurrentValue = if ($MissingApps.Count -eq 0) { + [PSCustomObject]@{ state = 'All template apps deployed' } + } else { + [PSCustomObject]@{ MissingApps = ($MissingApps.AppName -join ', ') } + } + + if ($Settings.remediate -eq $true) { + if ($MissingApps.Count -eq 0) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'All Intune application template apps are already deployed.' -sev Info + } else { + foreach ($App in $MissingApps) { + try { + # Map template appType to queue type used by New-CIPPIntuneAppDeployment + $QueueType = switch ($App.AppType) { + 'StoreApp' { 'WinGet' } + 'chocolateyApp' { 'Choco' } + 'win32ScriptApp' { 'Win32ScriptApp' } + 'mspApp' { 'MSPApp' } + 'officeApp' { 'OfficeApp' } + default { $App.AppType } + } + + # Build AppConfig in the same format as the apps queue + # Assignment info comes from the template's per-app config + $DeployConfig = $App.Config | ConvertTo-Json -Depth 100 | ConvertFrom-Json -Depth 100 + $DeployConfig | Add-Member -NotePropertyName 'type' -NotePropertyValue $QueueType -Force + $DeployConfig | Add-Member -NotePropertyName 'Applicationname' -NotePropertyValue $App.AppName -Force + # Compute assignTo the same way the HTTP handlers do + $AppAssignTo = if ($DeployConfig.AssignTo -eq 'customGroup') { $DeployConfig.CustomGroup } else { $DeployConfig.AssignTo } + $DeployConfig | Add-Member -NotePropertyName 'assignTo' -NotePropertyValue $AppAssignTo -Force + + $null = New-CIPPIntuneAppDeployment -AppConfig $DeployConfig -TenantFilter $Tenant -APIName 'Standards' + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Deployed Intune app '$($App.AppName)' ($($App.AppType)) from template '$($App.TemplateName)'." -sev Info + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to deploy Intune app '$($App.AppName)' from template '$($App.TemplateName)': $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + } + } + } + } + + if ($Settings.alert -eq $true) { + if ($MissingApps.Count -eq 0) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'All Intune application template apps are deployed.' -sev Info + } else { + $MissingList = $MissingApps.AppName -join ', ' + Write-StandardsAlert -message "The following Intune template apps are not deployed: $MissingList" -object (@{ 'Missing Apps' = $MissingList }) -tenant $Tenant -standardName 'IntuneAppTemplateDeploy' -standardId $Settings.standardId + } + } + + if ($Settings.report -eq $true) { + $StateIsCorrect = $MissingApps.Count -eq 0 + Set-CIPPStandardsCompareField -FieldName 'standards.IntuneAppTemplateDeploy' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant + Add-CIPPBPAField -FieldName 'IntuneAppTemplateDeploy' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant + } +} From 08ab039ccdaa149d3c0698d1dba627acd668ee58 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 25 May 2026 00:22:59 +0200 Subject: [PATCH 026/202] add filtering --- .../Public/Alerts/Get-CIPPAlertAppCertificateExpiry.ps1 | 2 ++ .../CIPPAlerts/Public/Alerts/Get-CIPPAlertAppSecretExpiry.ps1 | 1 + 2 files changed, 3 insertions(+) diff --git a/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertAppCertificateExpiry.ps1 b/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertAppCertificateExpiry.ps1 index 575930678050..299381674910 100644 --- a/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertAppCertificateExpiry.ps1 +++ b/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertAppCertificateExpiry.ps1 @@ -21,6 +21,7 @@ function Get-CIPPAlertAppCertificateExpiry { } $AppAlertData = foreach ($App in $appList) { + if ($App.displayName -match 'ConnectSyncProvisioning') { continue } if ($App.keyCredentials) { foreach ($Credential in $App.keyCredentials) { if ($Credential.endDateTime -lt $Now.AddDays(30) -and $Credential.endDateTime -gt $Now.AddDays(-7)) { @@ -42,6 +43,7 @@ function Get-CIPPAlertAppCertificateExpiry { } $SamlAlertData = foreach ($ServicePrincipal in $servicePrincipals) { + if ($ServicePrincipal.displayName -match 'ConnectSyncProvisioning') { continue } $ExpiryDate = $null if ($ServicePrincipal.preferredTokenSigningKeyEndDateTime) { $ExpiryDate = [datetime]$ServicePrincipal.preferredTokenSigningKeyEndDateTime diff --git a/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertAppSecretExpiry.ps1 b/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertAppSecretExpiry.ps1 index f8ade377536c..2a53835cc946 100644 --- a/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertAppSecretExpiry.ps1 +++ b/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertAppSecretExpiry.ps1 @@ -21,6 +21,7 @@ function Get-CIPPAlertAppSecretExpiry { $AlertData = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($App in $applist) { + if ($App.displayName -match 'ConnectSyncProvisioning') { continue } Write-Host "checking $($App.displayName)" if ($App.passwordCredentials) { foreach ($Credential in $App.passwordCredentials) { From c81b6a5c57a49d9dfec2728706fb7cc2098d5dfa Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 25 May 2026 00:23:02 +0200 Subject: [PATCH 027/202] add filtering --- .../Public/Alerts/Get-CIPPAlertAppCertificateExpiry.ps1 | 2 +- .../CIPPAlerts/Public/Alerts/Get-CIPPAlertAppSecretExpiry.ps1 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertAppCertificateExpiry.ps1 b/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertAppCertificateExpiry.ps1 index 299381674910..e31714f632dd 100644 --- a/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertAppCertificateExpiry.ps1 +++ b/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertAppCertificateExpiry.ps1 @@ -4,7 +4,7 @@ function Get-CIPPAlertAppCertificateExpiry { Entrypoint #> [CmdletBinding()] - Param ( + param ( [Parameter(Mandatory = $false)] [Alias('input')] $InputValue, diff --git a/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertAppSecretExpiry.ps1 b/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertAppSecretExpiry.ps1 index 2a53835cc946..20041ca7845c 100644 --- a/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertAppSecretExpiry.ps1 +++ b/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertAppSecretExpiry.ps1 @@ -4,7 +4,7 @@ function Get-CIPPAlertAppSecretExpiry { Entrypoint #> [CmdletBinding()] - Param ( + param ( [Parameter(Mandatory = $false)] [Alias('input')] $InputValue, From 33512c39360bd618749adf423d132b764e62db1f Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 25 May 2026 01:34:04 +0200 Subject: [PATCH 028/202] FIDO2 profile standards --- ...nvoke-CIPPStandardFIDO2PasskeyProfiles.ps1 | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardFIDO2PasskeyProfiles.ps1 diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardFIDO2PasskeyProfiles.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardFIDO2PasskeyProfiles.ps1 new file mode 100644 index 000000000000..37eb3e0402a3 --- /dev/null +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardFIDO2PasskeyProfiles.ps1 @@ -0,0 +1,159 @@ +function Invoke-CIPPStandardFIDO2PasskeyProfiles { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) FIDO2PasskeyProfiles + .SYNOPSIS + (Label) Configure FIDO2 Passkey Profile + .DESCRIPTION + (Helptext) Configures the default FIDO2 passkey profile including AAGUID allowlists, attestation enforcement, and passkey types for the tenant. + (DocsDescription) Manages the default FIDO2 passkey profile on the tenant authentication methods policy. Controls which authenticators (hardware keys, password managers, Microsoft Authenticator) are permitted via AAGUID allowlists, whether attestation is enforced, and which passkey types (device-bound, synced, or both) are allowed. This enables MSPs to centrally deploy phishing-resistant MFA configurations across tenants. + .NOTES + CAT + Entra (AAD) Standards + TAG + EXECUTIVETEXT + Configures the default passkey (FIDO2) profile that controls which authenticators users can register for phishing-resistant MFA. Supports allowlisting specific hardware keys (e.g., YubiKey models), password managers (e.g., 1Password), and Microsoft Authenticator by AAGUID, with control over attestation enforcement and passkey types. + ADDEDCOMPONENT + [{"type":"select","multiple":false,"name":"standards.FIDO2PasskeyProfiles.PasskeyTypes","label":"Allowed Passkey Types","options":[{"label":"Device-bound only","value":"deviceBound"},{"label":"Synced only","value":"synced"},{"label":"Both device-bound and synced","value":"deviceBound,synced"}],"required":true},{"type":"select","multiple":false,"name":"standards.FIDO2PasskeyProfiles.AttestationEnforcement","label":"Attestation Enforcement","options":[{"label":"Disabled (required for synced passkeys)","value":"disabled"},{"label":"Registration only","value":"registrationOnly"}],"required":true},{"type":"switch","name":"standards.FIDO2PasskeyProfiles.EnforceKeyRestrictions","label":"Enforce AAGUID Key Restrictions"},{"type":"select","multiple":false,"name":"standards.FIDO2PasskeyProfiles.EnforcementType","label":"Key Restriction Type","options":[{"label":"Allow listed AAGUIDs only","value":"allow"},{"label":"Block listed AAGUIDs","value":"block"}]},{"type":"textField","name":"standards.FIDO2PasskeyProfiles.AAGUIDs","label":"AAGUIDs (comma-separated list of authenticator AAGUIDs)"}] + IMPACT + Medium Impact + ADDEDDATE + 2026-05-25 + POWERSHELLEQUIVALENT + Graph API PATCH /policies/authenticationMethodsPolicy/authenticationMethodConfigurations/fido2 + RECOMMENDEDBY + "CIPP" + DISABLEDFEATURES + {"report":false,"warn":false,"remediate":false} + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + .LINK + https://docs.cipp.app/user-documentation/tenant/standards/alignment/templates/available-standards + #> + + param($Tenant, $Settings) + + $PasskeyTypes = $Settings.PasskeyTypes.value ?? $Settings.PasskeyTypes + + $AttestationEnforcement = $Settings.AttestationEnforcement.value ?? $Settings.AttestationEnforcement + + + $EnforceKeyRestrictions = [bool]$Settings.EnforceKeyRestrictions + $EnforcementType = $Settings.EnforcementType.value ?? $Settings.EnforcementType ?? 'allow' + + # Parse AAGUIDs from comma-separated string + $AAGUIDs = @() + if (-not [string]::IsNullOrWhiteSpace($Settings.AAGUIDs)) { + $AAGUIDs = @($Settings.AAGUIDs -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }) + } + + # Key restrictions require at least one AAGUID + if ($EnforceKeyRestrictions -and $AAGUIDs.Count -eq 0) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'FIDO2PasskeyProfiles: Key restrictions are enabled but no AAGUIDs specified. Provide at least one AAGUID or disable key restrictions.' -sev Error + return + } + + # Get current FIDO2 configuration + try { + $CurrentConfig = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/policies/authenticationmethodspolicy/authenticationMethodConfigurations/Fido2' -tenantid $Tenant -AsApp $true + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "FIDO2PasskeyProfiles: Could not retrieve current FIDO2 configuration. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + return $true + } + + # Find the default passkey profile + $DefaultProfileId = $CurrentConfig.defaultPasskeyProfile + $DefaultProfile = $CurrentConfig.passkeyProfiles | Where-Object { $_.id -eq $DefaultProfileId } + + # Determine compliance against the default profile + $StateIsCorrect = $false + if ($DefaultProfile) { + $ExistingAAGUIDs = @($DefaultProfile.keyRestrictions.aaGuids | Sort-Object) + $DesiredAAGUIDs = @($AAGUIDs | Sort-Object) + $AAGUIDsMatch = (-not (Compare-Object -ReferenceObject $DesiredAAGUIDs -DifferenceObject $ExistingAAGUIDs -ErrorAction SilentlyContinue)) + + $StateIsCorrect = ( + $DefaultProfile.passkeyTypes -eq $PasskeyTypes -and + $DefaultProfile.attestationEnforcement -eq $AttestationEnforcement -and + $DefaultProfile.keyRestrictions.isEnforced -eq $EnforceKeyRestrictions -and + $DefaultProfile.keyRestrictions.enforcementType -eq $EnforcementType -and + $AAGUIDsMatch + ) + } + + if ($Settings.remediate -eq $true) { + if ($StateIsCorrect) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'FIDO2PasskeyProfiles: Default passkey profile is already configured correctly.' -sev Info + } else { + try { + # Update the default profile in the profiles array, preserve all others + $ExistingProfiles = @($CurrentConfig.passkeyProfiles) + $UpdatedProfiles = foreach ($Profile in $ExistingProfiles) { + if ($Profile.id -eq $DefaultProfileId) { + @{ + id = $Profile.id + name = $Profile.name + passkeyTypes = $PasskeyTypes + attestationEnforcement = $AttestationEnforcement + keyRestrictions = @{ + isEnforced = $EnforceKeyRestrictions + enforcementType = $EnforcementType + aaGuids = $AAGUIDs + } + } + } else { + $Profile + } + } + + $Body = @{ + '@odata.type' = '#microsoft.graph.fido2AuthenticationMethodConfiguration' + passkeyProfiles = @($UpdatedProfiles) + } | ConvertTo-Json -Compress -Depth 10 + + Write-Host "FIDO2PasskeyProfiles: Request body: $Body" + + $null = New-GraphPostRequest -tenantid $Tenant -Uri 'https://graph.microsoft.com/beta/policies/authenticationmethodspolicy/authenticationMethodConfigurations/Fido2' -Type PATCH -Body $Body -ContentType 'application/json' -AsApp $true + Write-LogMessage -API 'Standards' -tenant $Tenant -message "FIDO2PasskeyProfiles: Successfully configured default passkey profile with $($AAGUIDs.Count) AAGUID(s), passkey types '$PasskeyTypes', attestation '$AttestationEnforcement'." -sev Info + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "FIDO2PasskeyProfiles: Failed to configure default passkey profile. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + } + } + } + + if ($Settings.alert -eq $true) { + if ($StateIsCorrect) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'FIDO2PasskeyProfiles: Default passkey profile is compliant.' -sev Info + } else { + $AlertDetails = if ($DefaultProfile) { + 'Default passkey profile exists but is not configured as expected.' + } else { + 'No default passkey profile found.' + } + Write-StandardsAlert -message "FIDO2PasskeyProfiles: $AlertDetails" -object $DefaultProfile -tenant $Tenant -standardName 'FIDO2PasskeyProfiles' -standardId $Settings.standardId + } + } + + if ($Settings.report -eq $true) { + $CurrentValue = [PSCustomObject]@{ + PasskeyTypes = $DefaultProfile.passkeyTypes ?? 'N/A' + AttestationEnforcement = $DefaultProfile.attestationEnforcement ?? 'N/A' + EnforceKeyRestrictions = $DefaultProfile.keyRestrictions.isEnforced ?? $false + EnforcementType = $DefaultProfile.keyRestrictions.enforcementType ?? 'N/A' + AAGUIDs = ($DefaultProfile.keyRestrictions.aaGuids ?? @()) -join ', ' + } + $ExpectedValue = [PSCustomObject]@{ + PasskeyTypes = $PasskeyTypes + AttestationEnforcement = $AttestationEnforcement + EnforceKeyRestrictions = $EnforceKeyRestrictions + EnforcementType = $EnforcementType + AAGUIDs = $AAGUIDs -join ', ' + } + Set-CIPPStandardsCompareField -FieldName 'standards.FIDO2PasskeyProfiles' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant + Add-CIPPBPAField -FieldName 'FIDO2PasskeyProfiles' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant + } +} From 03abdad414c58c3a3502068465c1ff6441fcc140 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 25 May 2026 01:58:21 +0200 Subject: [PATCH 029/202] add global var showing --- .../CIPP/Settings/Invoke-ExecCippReplacemap.ps1 | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCippReplacemap.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCippReplacemap.ps1 index 54d11633e42f..17e84e919a89 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCippReplacemap.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCippReplacemap.ps1 @@ -29,10 +29,23 @@ function Invoke-ExecCippReplacemap { switch ($Action) { 'List' { - $Variables = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq '$customerId'" + $Variables = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq '$customerId'" | ForEach-Object { + $_ | Add-Member -NotePropertyName 'Scope' -NotePropertyValue $(if ($customerId -eq 'AllTenants') { 'Global' } else { 'Tenant' }) -PassThru + } if (!$Variables) { $Variables = @() } + $IncludeGlobal = $Request.Query.includeGlobal ?? $Request.Body.includeGlobal + if ($IncludeGlobal -eq 'true' -and $customerId -ne 'AllTenants') { + $GlobalVariables = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq 'AllTenants'" | ForEach-Object { + $_ | Add-Member -NotePropertyName 'Scope' -NotePropertyValue 'Global' -PassThru + } + if ($GlobalVariables) { + $TenantVarNames = @($Variables | ForEach-Object { $_.RowKey }) + $GlobalVariables = @($GlobalVariables | Where-Object { $_.RowKey -notin $TenantVarNames }) + $Variables = @($Variables) + @($GlobalVariables) + } + } $Body = @{ Results = @($Variables) } } 'AddEdit' { From f09ce56acc6efba45ee1cb0b9dcbcf43c1224ac5 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Mon, 25 May 2026 11:58:16 +0800 Subject: [PATCH 030/202] Update New-TeamsRequest.ps1 --- Modules/CIPPCore/Public/GraphHelper/New-TeamsRequest.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/GraphHelper/New-TeamsRequest.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-TeamsRequest.ps1 index 64b85d354f46..48470f8cbd35 100644 --- a/Modules/CIPPCore/Public/GraphHelper/New-TeamsRequest.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/New-TeamsRequest.ps1 @@ -16,7 +16,8 @@ function New-TeamsRequest { $GraphToken = (Get-GraphToken -tenantid $TenantFilter).Authorization -replace 'Bearer ' $null = Connect-MicrosoftTeams -AccessTokens @($TeamsToken, $GraphToken) - & $Cmdlet @CmdParams + $Result = & $Cmdlet @CmdParams -ErrorAction Stop + $Result } else { Write-Error "Cmdlet $Cmdlet not found in MicrosoftTeams module" } From a0dab59a9091949721e42bd2f4c8cf78acc927cc Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Mon, 25 May 2026 14:35:20 +0800 Subject: [PATCH 031/202] domain fixes --- .../Domain Analyser/Push-DomainAnalyserTenant.ps1 | 4 ++-- .../Domain Analyser/Push-GetTenantDomains.ps1 | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-DomainAnalyserTenant.ps1 b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-DomainAnalyserTenant.ps1 index 7a5c8cfac91a..f06d3a1fae6b 100644 --- a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-DomainAnalyserTenant.ps1 +++ b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-DomainAnalyserTenant.ps1 @@ -99,8 +99,8 @@ function Push-DomainAnalyserTenant { } if ($OldDomain) { - $DomainObject.DkimSelectors = $OldDomain.DkimSelectors - $DomainObject.MailProviders = $OldDomain.MailProviders + $Domain.DkimSelectors = $OldDomain.DkimSelectors + $Domain.MailProviders = $OldDomain.MailProviders } } else { $Domain.TenantDetails = $TenantDetails diff --git a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-GetTenantDomains.ps1 b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-GetTenantDomains.ps1 index 071bb5289139..44d0ce7a9710 100644 --- a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-GetTenantDomains.ps1 +++ b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-GetTenantDomains.ps1 @@ -2,6 +2,6 @@ function Push-GetTenantDomains { Param($Item) $DomainTable = Get-CippTable -tablename 'Domains' $Filter = "PartitionKey eq 'TenantDomains' and TenantGUID eq '{0}'" -f $Item.TenantGUID - $Domains = Get-CIPPAzDataTableEntity @DomainTable -Filter $Filter -Property PartitionKey, RowKey | Select-Object RowKey, @{n = 'FunctionName'; exp = { 'DomainAnalyserDomain' } } + $Domains = Get-CIPPAzDataTableEntity @DomainTable -Filter $Filter -Property PartitionKey, RowKey, TenantId | Select-Object RowKey, @{n = 'FunctionName'; exp = { 'DomainAnalyserDomain' } }, @{n = 'TenantFilter'; exp = { $_.TenantId } } return @($Domains) } From 08b972ccac96794551db5c6dca9c2135914144e3 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Mon, 25 May 2026 15:33:49 +0800 Subject: [PATCH 032/202] timezone changes --- .../Initialize-CIPPTimezone.ps1 | 30 +++++++++++++++++++ .../CIPP/Settings/Invoke-ExecTimeSettings.ps1 | 3 +- 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 Modules/CIPPCore/Public/Authentication/Initialize-CIPPTimezone.ps1 diff --git a/Modules/CIPPCore/Public/Authentication/Initialize-CIPPTimezone.ps1 b/Modules/CIPPCore/Public/Authentication/Initialize-CIPPTimezone.ps1 new file mode 100644 index 000000000000..01f6400fb5e5 --- /dev/null +++ b/Modules/CIPPCore/Public/Authentication/Initialize-CIPPTimezone.ps1 @@ -0,0 +1,30 @@ +function Initialize-CIPPTimezone { + <# + .SYNOPSIS + Loads the configured timezone from storage, sets $env:CIPP_TIMEZONE, + and updates the scheduler timezone. + + .DESCRIPTION + Reads the TimeSettings row from the Config table. Sets the CIPP_TIMEZONE + environment variable to the configured timezone, or defaults to 'UTC' if not set or on error. + #> + [CmdletBinding()] + param() + + try { + $ConfigTable = Get-CIPPTable -tablename Config + $TimeSettings = Get-CIPPAzDataTableEntity @ConfigTable -Filter "PartitionKey eq 'TimeSettings' and RowKey eq 'TimeSettings'" -Property @('PartitionKey', 'RowKey', 'Timezone') -First 1 + if ($TimeSettings.Timezone) { + $null = [TimeZoneInfo]::FindSystemTimeZoneById($TimeSettings.Timezone) + [Craft.Services.PowerShellRunnerService]::SetProcessEnvVar('CIPP_TIMEZONE', $TimeSettings.Timezone) + [Craft.Services.PowerShellRunnerService]::SetProcessEnvVar('CraftTZ', $TimeSettings.Timezone) + Write-Information "[Timezone-Init] Timezone set to $($TimeSettings.Timezone)" + } else { + [Craft.Services.PowerShellRunnerService]::SetProcessEnvVar('CIPP_TIMEZONE', 'UTC') + Write-Information '[Timezone-Init] No timezone configured, defaulting to UTC' + } + } catch { + [Craft.Services.PowerShellRunnerService]::SetProcessEnvVar('CIPP_TIMEZONE', 'UTC') + Write-Warning "[Timezone-Init] Failed to load timezone, defaulting to UTC: $_" + } +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecTimeSettings.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecTimeSettings.ps1 index 1fcca265b612..c2769a2c9f02 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecTimeSettings.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecTimeSettings.ps1 @@ -31,7 +31,8 @@ function Invoke-ExecTimeSettings { $ConfigTable = Get-CIPPTable -tablename Config Add-CIPPAzDataTableEntity @ConfigTable -Entity $Config -Force | Out-Null $env:CIPP_TIMEZONE = $Timezone - + try { [Craft.Services.SchedulerBridge]::SetTimezone($Timezone) } catch { $null } + try { [Craft.Services.PowerShellRunnerService]::SetProcessEnvVar('CIPP_TIMEZONE', $Timezone) } catch { $null } Write-LogMessage -API 'ExecTimeSettings' -headers $Request.Headers -message "Updated time settings: Timezone=$Timezone" -Sev 'Info' return ([HttpResponseContext]@{ From d854e22d853449c8f45b44ed31c5e385508aba55 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Mon, 25 May 2026 11:49:28 +0200 Subject: [PATCH 033/202] feat: add function to remove users from admin roles --- .../Users/Invoke-ExecRemoveAdminRole.ps1 | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecRemoveAdminRole.ps1 diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecRemoveAdminRole.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecRemoveAdminRole.ps1 new file mode 100644 index 000000000000..1d10053353ca --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecRemoveAdminRole.ps1 @@ -0,0 +1,67 @@ +function Invoke-ExecRemoveAdminRole { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Identity.Role.ReadWrite + + .DESCRIPTION + Removes a user from an assigned Microsoft Entra admin role. + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + $TenantFilter = $Request.Body.tenantFilter.value ?? $Request.Body.tenantFilter + $RoleId = $Request.Body.RoleId.value ?? $Request.Body.RoleId + $RoleName = $Request.Body.RoleName.label ?? $Request.Body.RoleName.value ?? $Request.Body.RoleName + $Users = if ($Request.Body.Users) { @($Request.Body.Users) } else { @() } + + # Input validation + if ([string]::IsNullOrWhiteSpace($TenantFilter) -or [string]::IsNullOrWhiteSpace($RoleId) -or $Users.Count -eq 0) { + $Result = 'TenantFilter, RoleId, and Users are required to remove an admin role assignment.' + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev 'Error' + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = @{ 'Results' = @($Result) } + } + } + + $Results = [System.Collections.Generic.List[string]]::new() + $Failures = 0 + + foreach ($User in $Users) { + # Handle both roles and view user pages where user info is in different properties + $UserId = $User.value ?? $User.id ?? $User.UserId ?? $User + $UserName = $User.addedFields.userPrincipalName ?? $User.addedFields.displayName ?? $User.userPrincipalName ?? $User.displayName ?? $User.label ?? $User.UserName ?? $UserId + + if ([string]::IsNullOrWhiteSpace($UserId)) { + $Failures++ + $Result = "Skipped a $RoleName role removal request because UserId was empty." + $Results.Add($Result) + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev 'Error' + continue + } + + try { + $null = New-GraphPOSTRequest -type DELETE -uri "https://graph.microsoft.com/v1.0/directoryRoles/$RoleId/members/$UserId/`$ref" -tenantid $TenantFilter + $Result = "Successfully removed $UserName from admin role $RoleName." + $Results.Add($Result) + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev 'Info' + } catch { + $Failures++ + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to remove $UserName from admin role $RoleName. $($ErrorMessage.NormalizedError)" + $Results.Add($Result) + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev 'Error' -LogData $ErrorMessage + } + } + + $StatusCode = $Failures -gt 0 ? [HttpStatusCode]::InternalServerError : [HttpStatusCode]::OK + + return [HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{ 'Results' = @($Results) } + } +} From 46015ce7394fb954d357f8ebf26ef77a827626ad Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 25 May 2026 14:29:29 +0200 Subject: [PATCH 034/202] Add APv2 profile --- .../Invoke-CIPPStandardDevicePrepProfile.ps1 | 429 ++++++++++++++++++ 1 file changed, 429 insertions(+) create mode 100644 Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDevicePrepProfile.ps1 diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDevicePrepProfile.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDevicePrepProfile.ps1 new file mode 100644 index 000000000000..cfd22352bd49 --- /dev/null +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDevicePrepProfile.ps1 @@ -0,0 +1,429 @@ +function Invoke-CIPPStandardDevicePrepProfile { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) DevicePrepProfile + .SYNOPSIS + (Label) Deploy Device Prep Profile + .DESCRIPTION + (Helptext) Creates and manages a Windows Autopilot Device Preparation profile for streamlined device enrollment. + (DocsDescription) Deploys a Windows Autopilot Device Preparation profile through Intune configuration policies. This standard manages deployment mode, join type, account type, timeout, error messages, and optional device security group assignment. Optionally creates a new security group with the Intune Provisioning Client as owner. + .NOTES + CAT + Device Management Standards + TAG + "autopilot" + "device_prep" + "enrollment" + ADDEDCOMPONENT + {"type":"textField","name":"standards.DevicePrepProfile.ProfileName","label":"Profile Display Name","required":true} + {"type":"textField","name":"standards.DevicePrepProfile.ProfileDescription","label":"Profile Description","required":false} + {"type":"select","multiple":false,"name":"standards.DevicePrepProfile.DeploymentType","label":"Deployment Type","options":[{"label":"Single user","value":"0"},{"label":"Shared","value":"1"}]} + {"type":"select","multiple":false,"name":"standards.DevicePrepProfile.JoinType","label":"Join Type","options":[{"label":"Microsoft Entra join","value":"0"},{"label":"Microsoft Entra hybrid join","value":"1"}]} + {"type":"select","multiple":false,"name":"standards.DevicePrepProfile.AccountType","label":"Account Type","options":[{"label":"Standard user","value":"0"},{"label":"Administrator","value":"1"}]} + {"type":"number","name":"standards.DevicePrepProfile.Timeout","label":"Timeout (minutes)","defaultValue":60} + {"type":"textField","name":"standards.DevicePrepProfile.CustomErrorMessage","label":"Custom Error Message","required":false} + {"type":"switch","name":"standards.DevicePrepProfile.AllowSkip","label":"Allow users to skip setup after failure","defaultValue":false} + {"type":"switch","name":"standards.DevicePrepProfile.AllowDiagnostics","label":"Allow users to collect diagnostics","defaultValue":false} + {"type":"textField","name":"standards.DevicePrepProfile.DeviceGroupName","label":"Device Security Group Name (wildcard match)","required":false} + {"type":"switch","name":"standards.DevicePrepProfile.CreateNewGroup","label":"Create new group if group is not found","defaultValue":false} + {"type":"radio","name":"standards.DevicePrepProfile.AssignTo","label":"Policy Assignment","options":[{"label":"Do not assign","value":"none"},{"label":"All devices","value":"AllDevices"},{"label":"All users and devices","value":"AllDevicesAndUsers"}]} + IMPACT + High Impact + ADDEDDATE + 2025-05-25 + POWERSHELLEQUIVALENT + Graph API - deviceManagement/configurationPolicies + RECOMMENDEDBY + DISABLEDFEATURES + {"report":false,"warn":false,"remediate":false} + REQUIREDCAPABILITIES + "INTUNE_A" + "MDM_Services" + "EMS" + "SCCM" + "MICROSOFTINTUNEPLAN1" + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + .LINK + https://docs.cipp.app/user-documentation/tenant/standards/alignment/templates/available-standards + #> + + param($Tenant, $Settings) + + $TestResult = Test-CIPPStandardLicense -StandardName 'DevicePrepProfile' -TenantFilter $Tenant -Preset Intune + if ($TestResult -eq $false) { return $true } + + $ProfileName = $Settings.ProfileName + if ([string]::IsNullOrWhiteSpace($ProfileName)) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'DevicePrepProfile: ProfileName is empty, skipping.' -sev Error + return + } + + # Resolve setting values + $DeploymentMode = '0' # Device Prep only supports self-deploying mode + $DeploymentType = $Settings.DeploymentType.value ?? $Settings.DeploymentType ?? '0' + $JoinType = $Settings.JoinType.value ?? $Settings.JoinType ?? '0' + $AccountType = $Settings.AccountType.value ?? $Settings.AccountType ?? '0' + $Timeout = [int]($Settings.Timeout ?? 60) + $CustomErrorMessage = $Settings.CustomErrorMessage ?? "Contact your organization`u{2019}s support person for help." + $AllowSkip = if ($Settings.AllowSkip -eq $true) { '1' } else { '0' } + $AllowDiagnostics = if ($Settings.AllowDiagnostics -eq $true) { '1' } else { '0' } + $AssignTo = $Settings.AssignTo.value ?? $Settings.AssignTo ?? 'none' + + # Resolve device security group ID + $DeviceGroupId = '' + if (-not [string]::IsNullOrWhiteSpace($Settings.DeviceGroupName)) { + $GroupName = $Settings.DeviceGroupName + try { + $EscapedName = $GroupName -replace "'", "''" + $GroupFilter = [System.Uri]::EscapeDataString("startsWith(displayName,'$EscapedName') and mailEnabled eq false and securityEnabled eq true") + $MatchedGroups = @(New-GraphGetRequest -uri "https://graph.microsoft.com/beta/groups?`$select=id,displayName&`$filter=$GroupFilter" -tenantid $Tenant) + + if ($MatchedGroups.Count -gt 1) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "DevicePrepProfile: Multiple groups found matching '$GroupName', using first match '$($MatchedGroups[0].displayName)'" -sev Warning + $DeviceGroupId = $MatchedGroups[0].id + } elseif ($MatchedGroups.Count -eq 1) { + $DeviceGroupId = $MatchedGroups[0].id + Write-LogMessage -API 'Standards' -tenant $Tenant -message "DevicePrepProfile: Found group '$($MatchedGroups[0].displayName)' (ID: $DeviceGroupId)" -sev Info + } elseif ($Settings.CreateNewGroup -eq $true -and $Settings.remediate -eq $true) { + # Group not found — create it with Intune Provisioning Client as owner + $IntuneProvisioningAppId = 'f1346770-5b25-470b-88bd-d5744ab7952c' + $ServicePrincipal = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/servicePrincipals?`$filter=appId eq '$IntuneProvisioningAppId'&`$select=id" -tenantid $Tenant + $SpId = $ServicePrincipal.id + if ([string]::IsNullOrWhiteSpace($SpId)) { + # Service principal not found — instantiate it in the tenant + try { + $SpBody = @{ appId = $IntuneProvisioningAppId } | ConvertTo-Json -Compress + $CreatedSp = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/servicePrincipals' -tenantid $Tenant -body $SpBody -type POST + $SpId = $CreatedSp.id + Write-LogMessage -API 'Standards' -tenant $Tenant -message "DevicePrepProfile: Created Intune Provisioning Client service principal (ID: $SpId)" -sev Info + } catch { + $SpError = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "DevicePrepProfile: Failed to create Intune Provisioning Client service principal: $($SpError.NormalizedError)" -sev Error -LogData $SpError + return + } + } + + $GroupBody = @{ + displayName = $GroupName + description = 'Device Preparation security group managed by CIPP' + securityEnabled = $true + mailEnabled = $false + mailNickname = ($GroupName -replace '[^a-zA-Z0-9]', '') + (Get-Random -Maximum 9999) + 'owners@odata.bind' = @( + "https://graph.microsoft.com/v1.0/servicePrincipals/$SpId" + ) + } | ConvertTo-Json -Compress -Depth 10 + + $NewGroup = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/groups' -tenantid $Tenant -body $GroupBody -type POST + $DeviceGroupId = $NewGroup.id + Write-LogMessage -API 'Standards' -tenant $Tenant -message "DevicePrepProfile: Created security group '$GroupName' (ID: $DeviceGroupId) with Intune Provisioning Client as owner" -sev Info + } else { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "DevicePrepProfile: No security group found matching '$GroupName'" -sev Warning + } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "DevicePrepProfile: Failed to resolve device group: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + } + } + + # Build the expected configuration policy body + $PolicyBody = @{ + name = $ProfileName + description = $Settings.ProfileDescription ?? '' + roleScopeTagIds = @('0') + platforms = 'windows10' + technologies = 'enrollment' + templateReference = @{ + templateId = '80d33118-b7b4-40d8-b15f-81be745e053f_1' + } + settings = @( + @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSetting' + settingInstance = @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance' + settingDefinitionId = 'enrollment_autopilot_dpp_deploymentmode' + settingInstanceTemplateReference = @{ settingInstanceTemplateId = '5180aeab-886e-4589-97d4-40855c646315' } + choiceSettingValue = @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingValue' + children = @() + settingValueTemplateReference = @{ settingValueTemplateId = '5874c2f6-bcf1-463b-a9eb-bee64e2f2d82' } + value = "enrollment_autopilot_dpp_deploymentmode_$DeploymentMode" + } + } + } + @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSetting' + settingInstance = @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance' + settingDefinitionId = 'enrollment_autopilot_dpp_deploymenttype' + settingInstanceTemplateReference = @{ settingInstanceTemplateId = 'f4184296-fa9f-4b67-8b12-1723b3f8456b' } + choiceSettingValue = @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingValue' + children = @() + settingValueTemplateReference = @{ settingValueTemplateId = 'e0af022f-37f3-4a40-916d-1ab7281c88d9' } + value = "enrollment_autopilot_dpp_deploymenttype_$DeploymentType" + } + } + } + @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSetting' + settingInstance = @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance' + settingDefinitionId = 'enrollment_autopilot_dpp_jointype' + settingInstanceTemplateReference = @{ settingInstanceTemplateId = '6310e95d-6cfa-4d2f-aae0-1e7af12e2182' } + choiceSettingValue = @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingValue' + children = @() + settingValueTemplateReference = @{ settingValueTemplateId = '1fa84eb3-fcfa-4ed6-9687-0f3d486402c4' } + value = "enrollment_autopilot_dpp_jointype_$JoinType" + } + } + } + @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSetting' + settingInstance = @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance' + settingDefinitionId = 'enrollment_autopilot_dpp_accountype' + settingInstanceTemplateReference = @{ settingInstanceTemplateId = 'd4f2a840-86d5-4162-9a08-fa8cc608b94e' } + choiceSettingValue = @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingValue' + children = @() + settingValueTemplateReference = @{ settingValueTemplateId = 'bf13bb47-69ef-4e06-97c1-50c2859a49c2' } + value = "enrollment_autopilot_dpp_accountype_$AccountType" + } + } + } + @{ + settingInstance = @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSimpleSettingInstance' + settingDefinitionId = 'enrollment_autopilot_dpp_timeout' + settingInstanceTemplateReference = @{ settingInstanceTemplateId = '6dec0657-dfb8-4906-a7ee-3ac6ee1edecb' } + simpleSettingValue = @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationIntegerSettingValue' + value = $Timeout + settingValueTemplateReference = @{ settingValueTemplateId = '0bbcce5b-a55a-4e05-821a-94bf576d6cc8' } + } + } + } + @{ + settingInstance = @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSimpleSettingInstance' + settingDefinitionId = 'enrollment_autopilot_dpp_customerrormessage' + settingInstanceTemplateReference = @{ settingInstanceTemplateId = '2ddf0619-2b7a-46de-b29b-c6191e9dda6e' } + simpleSettingValue = @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationStringSettingValue' + value = $CustomErrorMessage + settingValueTemplateReference = @{ settingValueTemplateId = 'fe5002d5-fbe9-4920-9e2d-26bfc4b4cc97' } + } + } + } + @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSetting' + settingInstance = @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance' + settingDefinitionId = 'enrollment_autopilot_dpp_allowskip' + settingInstanceTemplateReference = @{ settingInstanceTemplateId = '2a71dc89-0f17-4ba9-bb27-af2521d34710' } + choiceSettingValue = @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingValue' + children = @() + settingValueTemplateReference = @{ settingValueTemplateId = 'a2323e5e-ac56-4517-8847-b0a6fdb467e7' } + value = "enrollment_autopilot_dpp_allowskip_$AllowSkip" + } + } + } + @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSetting' + settingInstance = @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance' + settingDefinitionId = 'enrollment_autopilot_dpp_allowdiagnostics' + settingInstanceTemplateReference = @{ settingInstanceTemplateId = 'e2b7a81b-f243-4abd-bce3-c1856345f405' } + choiceSettingValue = @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingValue' + children = @() + settingValueTemplateReference = @{ settingValueTemplateId = 'c59d26fd-3460-4b26-b47a-f7e202e7d5a3' } + value = "enrollment_autopilot_dpp_allowdiagnostics_$AllowDiagnostics" + } + } + } + @{ + settingInstance = @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSimpleSettingInstance' + settingDefinitionId = 'enrollment_autopilot_dpp_devicesecuritygroupids' + settingInstanceTemplateReference = @{ settingInstanceTemplateId = 'a46a50ab-3076-4968-9366-75a40dde950e' } + simpleSettingValue = @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationStringSettingValue' + value = $DeviceGroupId + settingValueTemplateReference = @{ settingValueTemplateId = '5f7d09e1-1a90-44ad-9c9f-ad90ba509e60' } + } + } + } + ) + } + + # Setting definition ID map for parsing current state + $ChoiceSettingMap = @{ + 'enrollment_autopilot_dpp_deploymentmode' = 'DeploymentMode' + 'enrollment_autopilot_dpp_deploymenttype' = 'DeploymentType' + 'enrollment_autopilot_dpp_jointype' = 'JoinType' + 'enrollment_autopilot_dpp_accountype' = 'AccountType' + 'enrollment_autopilot_dpp_allowskip' = 'AllowSkip' + 'enrollment_autopilot_dpp_allowdiagnostics' = 'AllowDiagnostics' + } + $SimpleSettingMap = @{ + 'enrollment_autopilot_dpp_timeout' = 'Timeout' + 'enrollment_autopilot_dpp_customerrormessage' = 'CustomErrorMessage' + 'enrollment_autopilot_dpp_devicesecuritygroupids' = 'DeviceGroupId' + } + + # Check existing policies + try { + $ExistingPolicies = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/configurationPolicies' -tenantid $Tenant + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "DevicePrepProfile: Failed to retrieve configuration policies: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + return + } + + $ExistingPolicy = $ExistingPolicies | Where-Object { $_.Name -eq $ProfileName } | Select-Object -First 1 + $PolicyExists = $null -ne $ExistingPolicy + + # Parse current settings + $CurrentParsed = @{} + if ($PolicyExists) { + try { + $PolicyDetail = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/configurationPolicies('$($ExistingPolicy.id)')?`$expand=settings" -tenantid $Tenant + foreach ($Setting in $PolicyDetail.settings) { + $Instance = $Setting.settingInstance + $DefId = $Instance.settingDefinitionId + + if ($ChoiceSettingMap.ContainsKey($DefId)) { + $CurrentParsed[$ChoiceSettingMap[$DefId]] = ($Instance.choiceSettingValue.value -split '_')[-1] + } elseif ($SimpleSettingMap.ContainsKey($DefId)) { + $CurrentParsed[$SimpleSettingMap[$DefId]] = $Instance.simpleSettingValue.value + } + } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "DevicePrepProfile: Failed to read policy settings: $($ErrorMessage.NormalizedError)" -sev Warning -LogData $ErrorMessage + } + } + + $CurrentValue = [PSCustomObject]@{ + PolicyExists = $PolicyExists + DeploymentMode = [string]($CurrentParsed.DeploymentMode ?? '') + DeploymentType = [string]($CurrentParsed.DeploymentType ?? '') + JoinType = [string]($CurrentParsed.JoinType ?? '') + AccountType = [string]($CurrentParsed.AccountType ?? '') + Timeout = [int]($CurrentParsed.Timeout ?? 0) + CustomErrorMessage = [string]($CurrentParsed.CustomErrorMessage ?? '') + AllowSkip = [string]($CurrentParsed.AllowSkip ?? '') + AllowDiagnostics = [string]($CurrentParsed.AllowDiagnostics ?? '') + DeviceGroupId = [string]($CurrentParsed.DeviceGroupId ?? '') + } + + $ExpectedValue = [PSCustomObject]@{ + PolicyExists = $true + DeploymentMode = $DeploymentMode + DeploymentType = $DeploymentType + JoinType = $JoinType + AccountType = $AccountType + Timeout = $Timeout + CustomErrorMessage = $CustomErrorMessage + AllowSkip = $AllowSkip + AllowDiagnostics = $AllowDiagnostics + DeviceGroupId = $DeviceGroupId + } + + # Determine compliance + $StateIsCorrect = $PolicyExists + if ($PolicyExists) { + $PropertiesToCompare = @('DeploymentMode', 'DeploymentType', 'JoinType', 'AccountType', 'AllowSkip', 'AllowDiagnostics') + foreach ($Prop in $PropertiesToCompare) { + if ([string]$CurrentValue.$Prop -ne [string]$ExpectedValue.$Prop) { + $StateIsCorrect = $false + break + } + } + if ($StateIsCorrect -and [int]$CurrentValue.Timeout -ne $ExpectedValue.Timeout) { $StateIsCorrect = $false } + if ($StateIsCorrect -and $CurrentValue.CustomErrorMessage -ne $ExpectedValue.CustomErrorMessage) { $StateIsCorrect = $false } + if ($StateIsCorrect -and $CurrentValue.DeviceGroupId -ne $ExpectedValue.DeviceGroupId) { $StateIsCorrect = $false } + } + + # Remediate + if ($Settings.remediate -eq $true) { + if ($StateIsCorrect) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "DevicePrepProfile: Profile '$ProfileName' already correctly configured" -sev Info + } else { + try { + # Delete drifted policy before recreating + if ($PolicyExists) { + $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/configurationPolicies('$($ExistingPolicy.id)')" -tenantid $Tenant -type DELETE + Write-LogMessage -API 'Standards' -tenant $Tenant -message "DevicePrepProfile: Deleted existing profile '$ProfileName' for recreation" -sev Info + } + + $Body = $PolicyBody | ConvertTo-Json -Compress -Depth 20 + $NewPolicy = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/configurationPolicies' -tenantid $Tenant -body $Body -type POST + + # Assign the policy if requested + if ($AssignTo -ne 'none' -and $NewPolicy.id) { + $AssignBody = switch ($AssignTo) { + 'AllDevices' { + @{ + assignments = @( + @{ + target = @{ + '@odata.type' = '#microsoft.graph.allDevicesAssignmentTarget' + } + } + ) + } + } + 'AllDevicesAndUsers' { + @{ + assignments = @( + @{ + target = @{ + '@odata.type' = '#microsoft.graph.allDevicesAssignmentTarget' + } + } + @{ + target = @{ + '@odata.type' = '#microsoft.graph.allLicensedUsersAssignmentTarget' + } + } + ) + } + } + } + if ($AssignBody) { + $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/configurationPolicies('$($NewPolicy.id)')/assign" -tenantid $Tenant -body ($AssignBody | ConvertTo-Json -Compress -Depth 10) -type POST + } + } + + Write-LogMessage -API 'Standards' -tenant $Tenant -message "DevicePrepProfile: Successfully deployed profile '$ProfileName'" -sev Info + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "DevicePrepProfile: Failed to deploy profile: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + } + } + } + + # Alert + if ($Settings.alert -eq $true) { + if ($StateIsCorrect) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "DevicePrepProfile: Profile '$ProfileName' is correctly configured" -sev Info + } else { + Write-StandardsAlert -message "Device Prep Profile '$ProfileName' is not correctly configured" -object $CurrentValue -tenant $Tenant -standardName 'DevicePrepProfile' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $Tenant -message "DevicePrepProfile: Profile '$ProfileName' is not correctly configured" -sev Info + } + } + + # Report + if ($Settings.report -eq $true) { + Set-CIPPStandardsCompareField -FieldName 'standards.DevicePrepProfile' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant + Add-CIPPBPAField -FieldName 'DevicePrepProfile' -FieldValue [bool]$StateIsCorrect -StoreAs bool -Tenant $Tenant + } +} From 71afcdd2be483a31473fcadaf239888671548787 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 26 May 2026 11:54:49 +0800 Subject: [PATCH 035/202] ExoTransportConfig cache type - fix for missing data used in test suites --- Config/CIPPDBCacheTypes.json | 5 +++ .../Public/Invoke-CIPPDBCacheCollection.ps1 | 1 + .../Set-CIPPDBCacheExoTransportConfig.ps1 | 35 +++++++++++++++++++ ...nvoke-CIPPStandardDisableBasicAuthSMTP.ps1 | 2 +- .../Invoke-CIPPStandardMessageExpiration.ps1 | 3 +- .../CIS/Identity/Invoke-CippTestCIS_6_5_4.ps1 | 4 +-- 6 files changed, 46 insertions(+), 4 deletions(-) create mode 100644 Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoTransportConfig.ps1 diff --git a/Config/CIPPDBCacheTypes.json b/Config/CIPPDBCacheTypes.json index 071eff75a431..eb833092befd 100644 --- a/Config/CIPPDBCacheTypes.json +++ b/Config/CIPPDBCacheTypes.json @@ -363,5 +363,10 @@ "type": "CopilotUserCountTrend", "friendlyName": "Copilot User Count Trend", "description": "Daily Copilot active user count trend (7-day period)" + }, + { + "type": "ExoTransportConfig", + "friendlyName": "Exchange Transport Config", + "description": "Exchange Online transport configuration including SMTP authentication settings" } ] diff --git a/Modules/CIPPCore/Public/Invoke-CIPPDBCacheCollection.ps1 b/Modules/CIPPCore/Public/Invoke-CIPPDBCacheCollection.ps1 index 9e666f8af279..c4af3e75d077 100644 --- a/Modules/CIPPCore/Public/Invoke-CIPPDBCacheCollection.ps1 +++ b/Modules/CIPPCore/Public/Invoke-CIPPDBCacheCollection.ps1 @@ -89,6 +89,7 @@ function Invoke-CIPPDBCacheCollection { 'ExoTenantAllowBlockList' 'OwaMailboxPolicy' 'ReportSubmissionPolicy' + 'ExoTransportConfig' ) ExchangeData = @( 'CASMailboxes' diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoTransportConfig.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoTransportConfig.ps1 new file mode 100644 index 000000000000..f606c288f020 --- /dev/null +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoTransportConfig.ps1 @@ -0,0 +1,35 @@ +function Set-CIPPDBCacheExoTransportConfig { + <# + .SYNOPSIS + Caches Exchange Online Transport Configuration + + .PARAMETER TenantFilter + The tenant to cache transport configuration for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter, + [string]$QueueId + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Transport configuration' -sev Debug + + $TransportConfig = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-TransportConfig' + + if ($TransportConfig) { + # TransportConfig returns a single object, wrap in array for consistency + $TransportConfigArray = @($TransportConfig) + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoTransportConfig' -Data $TransportConfigArray -AddCount + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached Exchange Transport configuration' -sev Debug + } + $TransportConfig = $null + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache Transport configuration: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 index 64c39c224c69..70637854ca19 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 @@ -50,7 +50,7 @@ function Invoke-CIPPStandardDisableBasicAuthSMTP { ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'DisableBasicAuthSMTP' try { - $CurrentInfo = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-TransportConfig' + $CurrentInfo = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoTransportConfig' | Select-Object -First 1 $SMTPusers = New-CippDbRequest -TenantFilter $Tenant -Type 'CASMailbox' | Where-Object { ($_.SmtpClientAuthenticationDisabled -eq $false) } } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardMessageExpiration.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardMessageExpiration.ps1 index 1bc199ba4537..4390c14ef392 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardMessageExpiration.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardMessageExpiration.ps1 @@ -41,7 +41,8 @@ function Invoke-CIPPStandardMessageExpiration { } #we're done. try { - $MessageExpiration = (New-ExoRequest -tenantid $Tenant -cmdlet 'Get-TransportConfig').messageExpiration + $TransportConfig = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoTransportConfig' | Select-Object -First 1 + $MessageExpiration = $TransportConfig.messageExpiration } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the MessageExpiration state for $Tenant. Error: $ErrorMessage" -Sev Error diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_5_4.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_5_4.ps1 index 69384d24eacc..ad4eac06c812 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_5_4.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_5_4.ps1 @@ -6,10 +6,10 @@ function Invoke-CippTestCIS_6_5_4 { param($Tenant) try { - $Org = Get-CIPPTestData -TenantFilter $Tenant -Type 'ExoOrganizationConfig' + $Org = Get-CIPPTestData -TenantFilter $Tenant -Type 'ExoTransportConfig' if (-not $Org) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_6_5_4' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'ExoOrganizationConfig cache not found.' -Risk 'High' -Name 'SMTP AUTH is disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication' + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_6_5_4' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'ExoTransportConfig cache not found.' -Risk 'High' -Name 'SMTP AUTH is disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication' return } From 1e63ebf0fc6b61a4873d4eb2f4f1b389f034676a Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 26 May 2026 15:09:22 +0800 Subject: [PATCH 036/202] Update Invoke-CIPPStandardsharingDomainRestriction.ps1 --- .../Invoke-CIPPStandardsharingDomainRestriction.ps1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardsharingDomainRestriction.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardsharingDomainRestriction.ps1 index 6bec4a3e9c6c..1517af22f389 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardsharingDomainRestriction.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardsharingDomainRestriction.ps1 @@ -121,13 +121,13 @@ function Invoke-CIPPStandardsharingDomainRestriction { $CurrentValue = @{ sharingDomainRestrictionMode = $CurrentState.sharingDomainRestrictionMode - sharingAllowedDomainList = $CurrentState.sharingAllowedDomainList - sharingBlockedDomainList = $CurrentState.sharingBlockedDomainList + sharingAllowedDomainList = @($CurrentState.sharingAllowedDomainList ?? @()) + sharingBlockedDomainList = @($CurrentState.sharingBlockedDomainList ?? @()) } $ExpectedValue = @{ sharingDomainRestrictionMode = $mode - sharingAllowedDomainList = if ($mode -eq 'allowList') { $SelectedDomains } else { @() } - sharingBlockedDomainList = if ($mode -eq 'blockList') { $SelectedDomains } else { @() } + sharingAllowedDomainList = @(if ($mode -eq 'allowList') { $SelectedDomains } else { @() }) + sharingBlockedDomainList = @(if ($mode -eq 'blockList') { $SelectedDomains } else { @() }) } Set-CIPPStandardsCompareField -FieldName 'standards.sharingDomainRestriction' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } From 59a0e15d1c11b1c8c33e0d04f3f6cd9dd96e23b0 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 26 May 2026 15:12:33 +0800 Subject: [PATCH 037/202] update application content type handling --- Shared/CIPPSharp/CIPPRestClient.cs | 12 +++++------- Shared/CIPPSharp/bin/CIPPSharp.dll | Bin 33280 -> 33280 bytes 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/Shared/CIPPSharp/CIPPRestClient.cs b/Shared/CIPPSharp/CIPPRestClient.cs index 2f0cf94fde7d..70a610110c12 100644 --- a/Shared/CIPPSharp/CIPPRestClient.cs +++ b/Shared/CIPPSharp/CIPPRestClient.cs @@ -564,13 +564,11 @@ public static async Task SendAsync( request.Content = new StringContent(body, encoding, mediaTypePart); - // Re-apply the full Content-Type (including charset) because - // StringContent's constructor strips parameters on some runtimes - if (ctParts.Length > 1) - { - request.Content.Headers.Remove("Content-Type"); - request.Content.Headers.TryAddWithoutValidation("Content-Type", effectiveCt); - } + // Re-apply the exact Content-Type the caller specified. + // StringContent's constructor auto-appends "; charset=utf-8" + // which breaks APIs that require a bare media type (e.g. "text/css"). + request.Content.Headers.Remove("Content-Type"); + request.Content.Headers.TryAddWithoutValidation("Content-Type", effectiveCt); } // Apply any deferred content headers (e.g. Content-Encoding) diff --git a/Shared/CIPPSharp/bin/CIPPSharp.dll b/Shared/CIPPSharp/bin/CIPPSharp.dll index d5d33d239f5533aba48891854bbb850111fab111..c35be9216c3f8a1a482f6ddd2d4344dbcd13979e 100644 GIT binary patch delta 5245 zcma)AX>?O%8h*c<<=)&Z%}tu6OK0mP!MUYH7zZc zO|f$k7!jutILfGJj;G>^+DJh~R73`+6`9sT71X0JIGj=O%=^8$gTnlooRjB!p6A`Z z-0z;Gw=L~LOS|ys8hzul;o;5=DXgG#_f2d-Wh`ju-2#xJ287XSB@41LwUtHGZyF`j zOaKtT0Izf_!tzwMOkF*qlKIqsjM&C*QJeg$S%S&}qyCWPe%xsd?E{dVHc!Y7kR9I7 zMM8G9k`O+?sa>%zYZYOsFZ>S5(Z5&hPBR&ycX`;Mm=%17xlkNxB#=5yQIyaj9!Y(8N(x^vA+!e|k#b!Z(Inr;Ws3r&@XicPUD{{Y?ahY0GIfy#2HB0Ce~ zIZl`23V+1gBq^>3ry`+UFF%4T%ju??cqV0q52LR*)z);6&Z#7-+tN2yB{`BpeP}zq z4sWO*Nlv-o$&#Dr@hvFc<^_Cx;Ce)dR|qM1Ek1unhgY$Q2b^Bv0lxm?8VBGLTv{D= z`i!^q2#QBtmr*x)gkwZ#fIDf9w9sIDi1*TvwB|d09g-%59DK+Z0w2Ny&NMYQ(;Z0* zeZf1(*&=;Vr;L{&IfM!F@*y7Z2~9Pa9X^R7e2P2%>B>3YUDF$Z#t%9$Mkr|^#7-$>du;-lHtf$_B`G- zp>Oz3GK8i&+;J!ox8lf9AI!RTOmg_27;&a7OOW`zbsj~ITS*zw6#juzGtyg$W;2g7 zRsAt5Q%Meo0E}5mYWONzeQNH=>shAy?#Ky(lBvc<-W{>vwKU_c@4*$%4r~AnDZI=8 z?!h$+1>WWK;{Ha2HMn{(84u&qYX$0dRR6{muivFU1A2fw)ON$=eWsyCzXtBTB9Mdy zYjMa^MgXtYo)4abjhEp2wJUr)Cpe8gj{L^wWg@?}vJ$KF!<{G$jw}Cs1YQF^T1gX|aRZr?}K-DSzQ?R*?D-_*1(MLH6Di%K5_3xKf}lQyJn6KBhNWw>ysF4 z7C0`^y@2gb>Rv}I)%PIo)bB^M7!D%xF$|s-K0@@7^RVF0CZk8{!J5)f({rB^95Rn_n`{FZ)0o6`$kuWEfH)K~n)wP)-4_riPO` zAQk+YI+rv?{d2xA5|qxH zH>&jozS>$j6BhID8yuVIT_leLLw;PXbS{-gK@q9V@Q!JPd=*q_s?^va7r=m~K5}lC z$HNOnylyj`a7IymPpZ@VtTU>9SKwno)m-SS9V1VGma#nVA?caqn0_Lx(bQ|nyX1+` z8CP_vCc#cpn<3BloIDBgisSvw${u+N=u6`2sClnE4Hjss!nRahTj-1Ildpj_rSS|y z!cuuQ9MTlu>l`Sf|C{q#rZ%6L{-3C}I8bJiseM>S^egZveZd=40$8 zaBHer7?3L=MNRX8hG!6Z%Hpc{_J)i6Vohjhbo z4b0V)Uw>9!05@xDgML_E2-TW8kE#yt($qM^S@}*_uBjDs*qn%E z2ET5D>@ITwtbk-v4@vFz4!94}HT5U^82LWP))c?-Rzjhs_>H#`CTgl5V+}A(Q(t4O z0p`RN-IR?`MJj%-jj(veWx;z%wc)RHj=2$<<1s1KR%pH-R%@!>Sc2-|OLdb_?daZE zJWGuH-MESEa<0;EVq2W2jZd(>Nf(Vf*_$3ktY(#NpSV^ykq{6!v3nB6>7QWj$r*+| zrt=2H3O3S^CngEQ_Eg<-SX(Z>h_!QsKK7~Y6V}J}*{;LP7j4&xQNirHS?psQ5^At& zwe3#vRbiJijQ!6gHAHY3PKt<&aM)@bR$*ID;YY<7V~Oin6gSt&cC!xiRx!$+N_bLy zPbf9+6!)_8p1oo(D|a3i53#R}8HPjbM(-SCT@G}lxBAv#aE|+4$E9N%otgzYK0evhtnd4#? z|CFEF)=J+%g}uo54ODrTAm{hQd#oVg5y=M6!(Sx_yZ|pD?jd{?QGhq#bxDF4yn(nM z-p0%iDDz`@DT3Bf_zM>NjSBjx;BzV%q=HjeAc2*=gBV~hA!e~dh&k+IDFAmv%#sC7 zaKRLWTVb4a4F2Kxn0XBTx%VWZ58gn`fCGrxptp>HLU1BZfHcIZkZp;KfmsxohgeSS zra~>;ZkbL^t6(|uYVvEzZy~3ZoHlaWF*9iGB0oz04)S}*iIKApzk`C-ebjT1oFQ_C zB4jbfKZlO-Bpu_QCO}RGIT_@XlQWf^spM3XQ_J{vTZy+3Z^tNZi1DpQ$=^YK4>>V% z_K`EdB0Oo3tRZR&0(Y1|8xnYHe6quK0dg{gHaKX(2Laq-IXP97Urm`U#9N7X5swn@ zAs!<>NXT?e*e9TCir`XCVj5!5T1~u_FiIFB9MI85iG!YVy

d>h;?o+3F{!jIc`2 zH{C+mMc6|)KsZPU2A;zZBez@pBm#tGgjIwsgzW~t)18KGkYnv7-fkqv$ekYJdRSoT zC1()vf{BUS;IJh?SWZ|?*edd9m$)9bSfb>_xDzoAqWH|hBp%iy9<=x+zPkW9ndFp_ zQ!eq$D&o}?Z6V%Dyo-30a(alz$R8wRCSDsb@hRnm)r65&TB3w8O!~^wi~OKvkQ}|t zc`M=tQ;cwskeRo^c}u`d?FeILejG9MldZkz1g(SQgQX2?nT2QiiI)+UlQS)XOVCK=w1H=a@2dvcE%CDB6cz}2r@p9tRtkj>JYUCG8EyP=``~!4Z z7x5NRO6{RQjQ9X?u<@v$Fw;hjZ2Z>iBJ8p8!2^VlK${>eBdj6>JNX>d`f2kc)6D?Q z@->LBxj64lqXs@4wOQ*XLvERf4+wDB?W^xhFi)Gv%b7)|1MahCX^_>R~wGD(zd(s*gMv_L9VbFU6Ywn{Ux06^lP zy|zmGU7TL-{X~!QmwPq%$bV(tyGj4+eT)B#xH8YJpk=#kNK_rV`dimw<62gXzq~{E zc1e9uEPlqSx=+pAl3};M{95+*&n8ZJbNloSj|6qi-5VB9H0o3_OMQP?X7|};M+~9- zg~f#nYwGgqYKv=%^Q((WO9~ehEvzZ1E2_({DJ&gVmseO+TvA-Ee%)|Z?Qe8Aq^*2! z@mD(M{?33K|E*_H{r6`Y(jf@`Zl&d8)@^&>h%h%F7UI)zAz}^G;eR3O@HeCwc`@We vH55T9lwfoLGM-U@@gjWZBQM0*II1Z`51**XXczab79U5b7Mdlhsz);1A-1}D|jilG9$GjpIShn)QW`63)+C7 zMt3@9#57}003W=sgH+45rbS2G9?WSex*6M!$H)q|n;W_Vl-}k%y=RfC} z8K$9GXlNE5e?TlVy>+Pd;biuQ)|YQ$r!>a0jGwLsNY;YFV6BX0vFo))7E{xn0(wnE z6u=1G9jZ{5!tU3WrDBry&(t-nTB}c6#sV4(W>0h3RGa4+mp!}ZsYlsj8~AjS}1wsCggd;6JLW%R)wYCl9++i;71L7phBJ6im6ssXD_wJeF|C znt8~KjapC*S#`)^^5j}n$6yJq2hr*i>i+~F3iV}(YJzI7`v~2dj}cVcqqDquQSCsm zY*3=AxZLqnmq+!~e8Ri=RnPt7kkIn$b|cGhd9~XHdRU_Neb6JSiK<(BY~W+%NzSBj zFNR%yr$4-(mzD_L3}wj_K3VlInaR(Kry@H2LRiHuIQWtQcd(uZTz+9OKc9CO0R#lM zUWc6y@RmM7^=bEqs$*%+wD4i>q&ic>{qZ5bi$hYE-2VR|sY2MvhkPOMAw1wp6&CaJ zdH3i`hNOnS;4PILiLTV8;64Pxn5onq;elkKz7or7j)Dhw?MrS2b3$sc<`|~~)nx5p zx<4@(-~GM%?gw%AgW8Mf*`t1w8&Z>Gn{nOHK+PG9xKe5pWd8X4h+>FGO-Ze<`H52t(&c*gq#b^wMH zHZy>`@jM6xKFIXq{u+b_@EpQq-1v*HM%2xyj3NMD?ZrN)P)ATF8!zp1J!P;qe`MXFFK9#2DKr2jNEZ0mQlH z1uO_D#0$-bWEJ{+Um<$@9RE$cm2d(%Zxf$Id?jKYs0k+#*So(%+?vR7E#Y|&=bOl{ zO8gmTy)N^KzsL(>IPyXB1q?ruD46rW<+hkrxWjt^NA@OiEGE9f&G}@)ThYmby-FhH z&q`RwR2Z1FPcChkn6OVC#&#G}%){Y!W6Sh}>B!a_0nLu8}))jND;V`xS5}i)w9D^=HFP=$tmpK^!YCLfj#? zm`yOqx=%L2Cg)RT2KTY&%%<2y>jex496Wr?zJQtFB;F1a?D1?wKNR46r-h@_#B)yA z56BEw2^{Y-bYQ!qhIbL8;(Lhe#P<=c#vVjIhQS8mGsFNnj~Mr1)e^%2a}cud9_Y6q z2ruJ};9GYD*LyR3h2f*}4~S37KWbIEh3ta1CU-)Ps2E{skUvVxjnyssOZab=4U5#?%8xJhf)nu;^)lyv< z+$pFYjw|1K*9;>*siby5f%}5l4?8pYgdLEg++a+Cm05AM(Y`_(lNVqEwdr|*Y1v9L z__E_GIp$vhDUhbC)BcE(0@=FS>IfMJK%TBXaAYe3V5F|9JR_7;7^|y=o)YcDyg=+a zB@Oz>YX!;A)wTG(0?-Oy`?%Vyt9nxDIq}$F%3P_dwG|R|pk~*rZsirB| zGi^vb^IreWF_g`@aXB{Wc`*bce`mq*XA`QFbhuMj>ApEiCK&VLvH7lAB^wGzt%CQ> z^OZqRuB)i2MahT5y86WRyfPAAFW_~n;E-!As_#j)`Zl5pjD*_?d2qShV-6WdLba~` zVb0brxnLlay9>qxDFNZ>W)3WyAk53s0zRmOriBChsYwkYFa zrmjluwc3(lf!KECYIqu zhjrp8KClT!us&r9D7xB!v1#DY)vXx22|T)5B7C8gL9(tsz|7cm7^ur)IIi3ZIl6MQ zyxH1Uiw6K>Yk!{TwJ3MzE!sOd#%GyBoIMe1fHuH8}(J823T zd)SS>Ny1!~t(=zT>OIGr{w3|D%s$FIN|{F~^A+HiZJCRM2g)tNYFAkM{13VQ>2fPu z;9LQ};@}$jYh39(xl4GK`)ZK$Ctw%LPk3Cm!>jmB!wIj$ zTZo$o-$oSRU3gcPp$oPmZijBn{D?9?!>9~<;T6pPJLUIM{z1y`r~EH5Uj`f7g&1US z#qgBDK1LkE_Q*k)1KX?_P!H$KSuh<&*@og5$JLgh_;s%hF#uZ;L-<)f6mr019SXz1 zjW`O@5XZp~#B0c(f>=V`#=#7zuuh}0z8@}me+qO~DesYeGbA%km_+=Ov&oMB5F+p-dd-bmO^*hP5Qz?ao;sE=V8 zh+K%G27*l#*Fdr@jhte_a*?mPfv}CRlkhNMKOq=-4kJt>3=$R-mJ>D*HXHdu*BRG9 zuB{t+tjF4HBFn_BPSZnBZS5wfAMu=-No!!YHAq-OSV7n*@o1a$5Ik*dC#Q>?e&h$O zOy*G$vB#Pw^Th?pNhhb6oDvx`W7cvK6%=kD-blQScsozBb`tL*zn_qqxgRvsjU%ie zY$R+a?82NA)^6lI)_!tCh4V(lbLK9>SU;VZWeuFQ1})T%u*<@4M;H3Zwr+H?ZT;kf zwTbVFm1m|AFD5J@XFOuIt(=?&!bU6Kzc%9Sl+#JPn=%i_Nc2+@*r>IQZ&w=eAn{`2 zCB(a0+zc_yHRpP(=m*?&D|K9rse~Y+0&!eJc zzhuZ-?enX?aql*@uqb}ceu;-y?uDpyfK_(%PFx*w+?Dg|qwC-N_Ud&FmUHAY^IZFZ6X>zi1=turJ;% zOs>M89cJS9mndQd{{9ff50D5H&{2S@8V~Lk;$IYoK^}}iuMmnbBZ9gTtA;}rvLdXC TYPqpqp*Y&1HhjlgBhr5X`G01f From e6b800b1c93bd1236bef7c6aacd194f4de8289f7 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 26 May 2026 15:34:21 +0800 Subject: [PATCH 038/202] remove rerun from alert --- .../Public/Alerts/Get-CIPPAlertQuarantineReleaseRequests.ps1 | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertQuarantineReleaseRequests.ps1 b/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertQuarantineReleaseRequests.ps1 index cc6795457cf1..e53db602ca35 100644 --- a/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertQuarantineReleaseRequests.ps1 +++ b/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertQuarantineReleaseRequests.ps1 @@ -11,11 +11,6 @@ $TenantFilter ) - #Add rerun protection: This Monitor can only run once every hour. - $Rerun = Test-CIPPRerun -TenantFilter $TenantFilter -Type 'ExchangeMonitor' -API 'Get-CIPPAlertQuarantineReleaseRequests' - if ($Rerun) { - return - } $HasLicense = Test-CIPPStandardLicense -StandardName 'QuarantineReleaseRequests' -TenantFilter $TenantFilter -Preset Exchange if (-not $HasLicense) { From 97dc672c748a5152c5a7438588f6ab6847b4ac26 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 26 May 2026 23:33:37 +0800 Subject: [PATCH 039/202] user sync --- Config/CIPPTimers.json | 9 + .../Timer Functions/Start-UserSyncTimer.ps1 | 194 ++++++++++++++++++ .../CIPP/Settings/Invoke-ExecCIPPUsers.ps1 | 30 ++- .../CIPP/Settings/Invoke-ListCIPPUsers.ps1 | 26 ++- 4 files changed, 256 insertions(+), 3 deletions(-) create mode 100644 Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-UserSyncTimer.ps1 diff --git a/Config/CIPPTimers.json b/Config/CIPPTimers.json index 077f7d53fb0c..0802c9863096 100644 --- a/Config/CIPPTimers.json +++ b/Config/CIPPTimers.json @@ -273,5 +273,14 @@ "Priority": 30, "RunOnProcessor": false, "IsSystem": true + }, + { + "Id": "7e2a9b4c-1d5f-4a8e-b3c6-0f9d2e7a4b1c", + "Command": "Start-UserSyncTimer", + "Description": "Sync partner tenant users and group-based roles into allowedUsers table", + "Cron": "0 */15 * * * *", + "Priority": 11, + "RunOnProcessor": false, + "IsSystem": true } ] diff --git a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-UserSyncTimer.ps1 b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-UserSyncTimer.ps1 new file mode 100644 index 000000000000..f4578bccecdf --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-UserSyncTimer.ps1 @@ -0,0 +1,194 @@ +function Start-UserSyncTimer { + <# + .SYNOPSIS + Sync partner tenant users into the allowedUsers table + .DESCRIPTION + Pulls users from the partner tenant via Graph, resolves their Entra group memberships + against AccessRoleGroups, and upserts into allowedUsers with auto-derived roles. + Manual role assignments are preserved in a separate column and merged at compute time. + .FUNCTIONALITY + Entrypoint + #> + + [CmdletBinding(SupportsShouldProcess = $true)] + param() + + if (-not $PSCmdlet.ShouldProcess('Start-UserSyncTimer', 'Sync partner tenant users to allowedUsers table')) { + return + } + + $ApiName = 'UserSync' + + try { + Write-LogMessage -API $ApiName -tenant 'none' -message 'Starting user sync from partner tenant.' -sev Info + + # Load the role-to-group mappings + $AccessGroupsTable = Get-CippTable -TableName AccessRoleGroups + $AccessGroups = @(Get-CIPPAzDataTableEntity @AccessGroupsTable -Filter "PartitionKey eq 'AccessRoleGroups'") + + # Get the group IDs we care about + $RoleGroupIds = @($AccessGroups | ForEach-Object { $_.GroupId } | Where-Object { $_ }) + + # Build a lookup: GroupId -> Role names (a group can map to multiple roles) + $GroupToRoles = @{} + foreach ($Mapping in $AccessGroups) { + if ($Mapping.GroupId) { + if (-not $GroupToRoles.ContainsKey($Mapping.GroupId)) { + $GroupToRoles[$Mapping.GroupId] = [System.Collections.Generic.List[string]]::new() + } + $GroupToRoles[$Mapping.GroupId].Add($Mapping.RowKey) + } + } + + # Fetch members of each role group from the partner tenant + # Use transitiveMembers to catch nested group memberships + $UserRoleMap = @{} # UPN -> HashSet of auto roles + + foreach ($GroupId in $RoleGroupIds) { + try { + $Members = @(New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/groups/$GroupId/transitiveMembers?`$select=id,userPrincipalName,mail,accountEnabled&`$top=999" -NoAuthCheck $true -AsApp $true) + $UserMembers = @($Members | Where-Object { $_.'@odata.type' -eq '#microsoft.graph.user' -and $_.accountEnabled -eq $true }) + + $RolesForGroup = $GroupToRoles[$GroupId] + + foreach ($Member in $UserMembers) { + $Upn = $Member.userPrincipalName + if ([string]::IsNullOrWhiteSpace($Upn)) { continue } + $Upn = $Upn.Trim().ToLower() + + if (-not $UserRoleMap.ContainsKey($Upn)) { + $UserRoleMap[$Upn] = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) + } + foreach ($Role in $RolesForGroup) { + [void]$UserRoleMap[$Upn].Add($Role) + } + } + } catch { + $ErrorData = Get-CippException -Exception $_ + Write-LogMessage -API $ApiName -tenant 'none' -message "Failed to fetch members of group $GroupId : $($ErrorData.NormalizedError)" -sev Warning -LogData $ErrorData + } + } + + if ($UserRoleMap.Count -eq 0 -and $RoleGroupIds.Count -gt 0) { + Write-LogMessage -API $ApiName -tenant 'none' -message 'No users found in any role groups.' -sev Info + } elseif ($RoleGroupIds.Count -eq 0) { + Write-LogMessage -API $ApiName -tenant 'none' -message 'No Entra groups mapped to roles — will clean up any stale auto-provisioned users.' -sev Info + } + + # Load existing allowedUsers table + $UsersTable = Get-CippTable -tablename 'allowedUsers' + $ExistingUsers = @(Get-CIPPAzDataTableEntity @UsersTable | Where-Object { -not $_.RowKey.StartsWith('_') }) + + # Build lookup of existing users + $ExistingLookup = @{} + foreach ($Existing in $ExistingUsers) { + $ExistingLookup[$Existing.RowKey.ToLower()] = $Existing + } + + $Now = (Get-Date).ToUniversalTime().ToString('o') + $UpsertCount = 0 + $RemoveCount = 0 + $EntitiesToUpsert = [System.Collections.Generic.List[object]]::new() + + # Upsert users from Graph + foreach ($Upn in $UserRoleMap.Keys) { + $AutoRoles = @($UserRoleMap[$Upn] | Sort-Object) + + $ManualRoles = @() + $Source = 'Auto' + + if ($ExistingLookup.ContainsKey($Upn)) { + $Existing = $ExistingLookup[$Upn] + + # Preserve manual roles if they exist + if ($Existing.ManualRoles) { + try { + $ManualRoles = @($Existing.ManualRoles | ConvertFrom-Json -ErrorAction Stop) + } catch { + $ManualRoles = @() + } + } + + # If user was previously manual-only and now also auto, mark as Both + if ($ManualRoles.Count -gt 0) { + $Source = 'Both' + } + } + + # Compute effective roles = union of auto + manual + $EffectiveRoles = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) + foreach ($Role in $AutoRoles) { [void]$EffectiveRoles.Add($Role) } + foreach ($Role in $ManualRoles) { [void]$EffectiveRoles.Add($Role) } + $EffectiveRolesArray = @($EffectiveRoles | Sort-Object) + + $Entity = @{ + PartitionKey = 'User' + RowKey = $Upn + Roles = [string]($EffectiveRolesArray | ConvertTo-Json -Compress -AsArray) + AutoRoles = [string]($AutoRoles | ConvertTo-Json -Compress -AsArray) + ManualRoles = [string](($ManualRoles.Count -gt 0 ? $ManualRoles : @()) | ConvertTo-Json -Compress -AsArray) + Source = $Source + LastSync = $Now + } + + $EntitiesToUpsert.Add($Entity) + $UpsertCount++ + } + + # Handle users that were auto-provisioned but are no longer in any role group + foreach ($Existing in $ExistingUsers) { + $ExistingUpn = $Existing.RowKey.ToLower() + if ($UserRoleMap.ContainsKey($ExistingUpn)) { continue } # Still in a group, already handled + + if ($Existing.Source -eq 'Auto') { + # Purely auto-provisioned user no longer in any group — remove + Remove-AzDataTableEntity -Force @UsersTable -Entity $Existing + $RemoveCount++ + } elseif ($Existing.Source -eq 'Both') { + # Was both auto + manual — clear auto roles, keep manual only + $ManualRoles = @() + if ($Existing.ManualRoles) { + try { + $ManualRoles = @($Existing.ManualRoles | ConvertFrom-Json -ErrorAction Stop) + } catch { + $ManualRoles = @() + } + } + + if ($ManualRoles.Count -gt 0) { + $Entity = @{ + PartitionKey = 'User' + RowKey = $Existing.RowKey + Roles = [string]($ManualRoles | ConvertTo-Json -Compress -AsArray) + AutoRoles = '[]' + ManualRoles = [string]($ManualRoles | ConvertTo-Json -Compress -AsArray) + Source = 'Manual' + LastSync = $Now + } + $EntitiesToUpsert.Add($Entity) + } else { + # No manual roles either — remove + Remove-AzDataTableEntity -Force @UsersTable -Entity $Existing + $RemoveCount++ + } + } + # Source = 'Manual' (or unset) — leave untouched, these are purely manual entries + } + + # Batch upsert + if ($EntitiesToUpsert.Count -gt 0) { + foreach ($Entity in $EntitiesToUpsert) { + Add-CIPPAzDataTableEntity @UsersTable -Entity $Entity -Force + } + } + + # Invalidate CRAFT's in-memory user cache so changes apply + try { [Craft.Services.AuthBridge]::InvalidateUsers() } catch {} + + Write-LogMessage -API $ApiName -tenant 'none' -message "User sync completed: $UpsertCount users synced, $RemoveCount auto-only users removed." -sev Info + + } catch { + $ErrorData = Get-CippException -Exception $_ + Write-LogMessage -API $ApiName -tenant 'none' -message "User sync failed: $($ErrorData.NormalizedError)" -sev Error -LogData $ErrorData + } +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCIPPUsers.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCIPPUsers.ps1 index 1220df4bad43..dfc78cd1a542 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCIPPUsers.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCIPPUsers.ps1 @@ -45,13 +45,41 @@ function Invoke-ExecCIPPUsers { } } + # Check if user already exists to preserve auto-synced roles + $ExistingEntity = Get-CIPPAzDataTableEntity @Table -Filter "RowKey eq '$UPN'" + $AutoRoles = @() + $Source = 'Manual' + + if ($ExistingEntity -and $ExistingEntity.AutoRoles) { + try { + $AutoRoles = @($ExistingEntity.AutoRoles | ConvertFrom-Json -ErrorAction Stop) + } catch { + $AutoRoles = @() + } + if ($AutoRoles.Count -gt 0) { + $Source = 'Both' + } + } + + # Compute effective roles = union of manual + auto + $EffectiveRoles = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) + foreach ($R in $Roles) { [void]$EffectiveRoles.Add($R) } + foreach ($R in $AutoRoles) { [void]$EffectiveRoles.Add($R) } + $EffectiveRolesArray = @($EffectiveRoles | Sort-Object) + $Entity = @{ PartitionKey = 'User' RowKey = $UPN - Roles = [string](@($Roles) | ConvertTo-Json -Compress -AsArray) + Roles = [string]($EffectiveRolesArray | ConvertTo-Json -Compress -AsArray) + ManualRoles = [string](@($Roles) | ConvertTo-Json -Compress -AsArray) + AutoRoles = [string]($AutoRoles | ConvertTo-Json -Compress -AsArray) + Source = $Source } Add-CIPPAzDataTableEntity @Table -Entity $Entity -Force | Out-Null + # Trigger a user sync to reconcile auto + manual roles + try { Start-UserSyncTimer } catch {} + # Invalidate the in-memory user cache so changes apply immediately try { [Craft.Services.AuthBridge]::InvalidateUsers() } catch {} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListCIPPUsers.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListCIPPUsers.ps1 index 5ed1dff766e2..0e60dcb07c25 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListCIPPUsers.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListCIPPUsers.ps1 @@ -39,9 +39,31 @@ function Invoke-ListCIPPUsers { } } + $ParsedManualRoles = @() + if ($User.ManualRoles) { + try { + $ParsedManualRoles = @($User.ManualRoles | ConvertFrom-Json -ErrorAction Stop) + } catch { + $ParsedManualRoles = @() + } + } + + $ParsedAutoRoles = @() + if ($User.AutoRoles) { + try { + $ParsedAutoRoles = @($User.AutoRoles | ConvertFrom-Json -ErrorAction Stop) + } catch { + $ParsedAutoRoles = @() + } + } + $UserList.Add([pscustomobject]@{ - UPN = $User.RowKey - Roles = $ParsedRoles + UPN = $User.RowKey + Roles = $ParsedRoles + ManualRoles = $ParsedManualRoles + AutoRoles = $ParsedAutoRoles + Source = $User.Source + LastSync = $User.LastSync }) } From 359633a42f5c208fd3b7e85f5903833e96e9c1c4 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Tue, 26 May 2026 17:58:54 +0200 Subject: [PATCH 040/202] fix: ensure tenant groups skips cache so they dont alternate anymore when refreshed --- .../Tenant/Administration/Tenant/Invoke-ListTenantDetails.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenantDetails.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenantDetails.ps1 index 98bb7a0641e6..37c6ca5eb34f 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenantDetails.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenantDetails.ps1 @@ -24,7 +24,7 @@ Function Invoke-ListTenantDetails { $customProperties = Get-TenantProperties -customerId $TenantFilter $org | Add-Member -MemberType NoteProperty -Name 'customProperties' -Value $customProperties - $Groups = (Get-TenantGroups -TenantFilter $TenantFilter) ?? @() + $Groups = (Get-TenantGroups -TenantFilter $TenantFilter -SkipCache) ?? @() $org | Add-Member -MemberType NoteProperty -Name 'Groups' -Value @($Groups) $StatusCode = [HttpStatusCode]::OK From 49d629e51778c237f1e5fb4580493e32eb93937c Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 27 May 2026 00:31:43 +0800 Subject: [PATCH 041/202] Update Get-CippApiAuth.ps1 --- Modules/CIPPCore/Public/Authentication/Get-CippApiAuth.ps1 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Authentication/Get-CippApiAuth.ps1 b/Modules/CIPPCore/Public/Authentication/Get-CippApiAuth.ps1 index 6d822746d6cf..faea057c4fa1 100644 --- a/Modules/CIPPCore/Public/Authentication/Get-CippApiAuth.ps1 +++ b/Modules/CIPPCore/Public/Authentication/Get-CippApiAuth.ps1 @@ -41,9 +41,12 @@ function Get-CippApiAuth { } } + $ExtractedTenantId = $Issuer -replace 'https://sts.windows.net/', '' -replace 'https://login.microsoftonline.com/', '' -replace '/v2.0', '' + $TenantId = if ($ExtractedTenantId -eq 'common') { $env:TenantID } else { $ExtractedTenantId } + [PSCustomObject]@{ ApiUrl = "https://$($env:WEBSITE_HOSTNAME)" - TenantID = $Issuer -replace 'https://sts.windows.net/', '' -replace 'https://login.microsoftonline.com/', '' -replace '/v2.0', '' + TenantID = $TenantId ClientIDs = $AllowedApps Enabled = $AAD.enabled } From 22902b0b81b33f8785715fd37ad176e60afade3b Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 27 May 2026 01:51:03 +0800 Subject: [PATCH 042/202] api fixes --- .../Authentication/New-CIPPAPIConfig.ps1 | 83 ++++++++++---- .../Repair-CippApiIdentifierUri.ps1 | 101 ++++++++++++++++++ .../CIPP/Settings/Invoke-ExecApiClient.ps1 | 34 ++++++ 3 files changed, 198 insertions(+), 20 deletions(-) create mode 100644 Modules/CIPPCore/Public/Authentication/Repair-CippApiIdentifierUri.ps1 diff --git a/Modules/CIPPCore/Public/Authentication/New-CIPPAPIConfig.ps1 b/Modules/CIPPCore/Public/Authentication/New-CIPPAPIConfig.ps1 index 80480d9a23ab..ebf9d8120e0e 100644 --- a/Modules/CIPPCore/Public/Authentication/New-CIPPAPIConfig.ps1 +++ b/Modules/CIPPCore/Public/Authentication/New-CIPPAPIConfig.ps1 @@ -22,6 +22,20 @@ function New-CIPPAPIConfig { if ($AppId) { $APIApp = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/applications(appid='$($AppId)')" -NoAuthCheck $true -AsApp $true Write-Information "Found existing app with AppId $AppId" + + # Validate and repair identifier URI for existing apps + try { + $RepairResult = Repair-CippApiIdentifierUri -AppId $AppId -ApplicationObjectId $APIApp.id + if ($RepairResult.Fixed) { + Write-Information "Repaired identifier URI: $($RepairResult.Message)" + Write-LogMessage -headers $Headers -API $APINAME -tenant 'None' -message "Repaired identifier URI for app $AppId $($RepairResult.Message)" -Sev 'Info' + } else { + Write-Information "Identifier URI validation: $($RepairResult.Message)" + } + } catch { + Write-Warning "Failed to validate/repair identifier URI for existing app: $($_.Exception.Message)" + # Don't fail the whole operation if URI repair fails + } } else { $CreateBody = @{ api = @{ @@ -122,29 +136,58 @@ function New-CIPPAPIConfig { if (-not $APIPassword) { throw 'Failed to create application password after retries. The app may not have replicated yet; wait a moment and retry.' } - Write-Information 'Adding App URL' + + Write-Information 'Adding Application Identifier URI' $Step = 'Adding Application Identifier URI' - $IdentifierUriBody = "{`"identifierUris`":[`"api://$($APIApp.appId)`"]}" - $IdentifierUriUpdated = $false - for ($Attempt = 1; $Attempt -le 6; $Attempt++) { - try { - New-GraphPOSTRequest -uri "https://graph.microsoft.com/v1.0/applications/$($APIApp.id)" -AsApp $true -NoAuthCheck $true -type PATCH -body $IdentifierUriBody -maxRetries 3 | Out-Null - $IdentifierUriUpdated = $true - break - } catch { - $IsNotReplicatedYet = $_.Exception.Message -match "Resource '.*' does not exist or one of its queried reference-property objects are not present" - if ($IsNotReplicatedYet -and $Attempt -lt 6) { - $DelaySeconds = 3 - Write-Information "Application object not yet replicated for identifier URI update (attempt $Attempt of 6). Retrying in $DelaySeconds second(s)." - Start-Sleep -Seconds $DelaySeconds - try { - $APIApp = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/applications(appid='$($APIApp.appId)')" -NoAuthCheck $true -AsApp $true - } catch { - Write-Information "Application lookup retry failed before identifier URI retry: $($_.Exception.Message)" + $DesiredIdentifierUri = "api://$($APIApp.appId)" + + # Check if identifier URI already exists + if ($APIApp.identifierUris -contains $DesiredIdentifierUri) { + Write-Information "Application Identifier URI '$DesiredIdentifierUri' already set, skipping." + $IdentifierUriUpdated = $true + } else { + Write-Information "Setting Application Identifier URI to '$DesiredIdentifierUri'" + $IdentifierUriBody = @{ + identifierUris = @($DesiredIdentifierUri) + } + $IdentifierUriUpdated = $false + for ($Attempt = 1; $Attempt -le 6; $Attempt++) { + try { + New-GraphPOSTRequest -uri "https://graph.microsoft.com/v1.0/applications/$($APIApp.id)" -AsApp $true -NoAuthCheck $true -type PATCH -body $IdentifierUriBody -maxRetries 3 | Out-Null + Write-Information "Successfully set identifier URI on attempt $Attempt" + $IdentifierUriUpdated = $true + break + } catch { + $ErrorMsg = $_.Exception.Message + Write-Information "Identifier URI update attempt $Attempt failed: $ErrorMsg" + + $IsNotReplicatedYet = $ErrorMsg -match "Resource '.*' does not exist or one of its queried reference-property objects are not present" + $IsConflict = $ErrorMsg -match "Another object with the same value for property identifierUris already exists" -or $ErrorMsg -match "Property identifierUris is invalid" + + if ($IsConflict) { + Write-Warning "Identifier URI conflict detected: $ErrorMsg" + Write-Information "This may indicate the URI is already in use by another application or the app registration needs manual cleanup." + throw } - continue + + if ($IsNotReplicatedYet -and $Attempt -lt 6) { + $DelaySeconds = 3 + Write-Information "Application object not yet replicated for identifier URI update (attempt $Attempt of 6). Retrying in $DelaySeconds second(s)." + Start-Sleep -Seconds $DelaySeconds + try { + $APIApp = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/applications(appid='$($APIApp.appId)')" -NoAuthCheck $true -AsApp $true + Write-Information "Re-fetched app: identifierUris=$($APIApp.identifierUris -join ', ')" + } catch { + Write-Information "Application lookup retry failed before identifier URI retry: $($_.Exception.Message)" + } + continue + } + + if ($Attempt -ge 6) { + Write-Warning "Failed to set identifier URI after $Attempt attempts. Last error: $ErrorMsg" + } + throw } - throw } } diff --git a/Modules/CIPPCore/Public/Authentication/Repair-CippApiIdentifierUri.ps1 b/Modules/CIPPCore/Public/Authentication/Repair-CippApiIdentifierUri.ps1 new file mode 100644 index 000000000000..1f557db756ee --- /dev/null +++ b/Modules/CIPPCore/Public/Authentication/Repair-CippApiIdentifierUri.ps1 @@ -0,0 +1,101 @@ +function Repair-CippApiIdentifierUri { + <# + .SYNOPSIS + Validates and repairs the Application ID URI (api://{appId}) for a CIPP API client + .DESCRIPTION + Checks if an application has the correct identifier URI set (api://{appId}) and fixes it if missing or incorrect. + This is required for client_credentials (app-only) authentication to work properly with EasyAuth. + .PARAMETER AppId + The Application (Client) ID of the app to check/repair + .PARAMETER ApplicationObjectId + Optional. The object ID of the application. If not provided, will be looked up. + .EXAMPLE + Repair-CippApiIdentifierUri -AppId '12345678-1234-1234-1234-123456789012' + .OUTPUTS + PSCustomObject with properties: + - Fixed: boolean indicating if a fix was applied + - PreviousUri: the previous identifier URI (if any) + - CurrentUri: the current/fixed identifier URI + - Message: description of what happened + #> + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(Mandatory = $true)] + [string]$AppId, + + [Parameter(Mandatory = $false)] + [string]$ApplicationObjectId + ) + + try { + # Get the application details + $App = if ($ApplicationObjectId) { + New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/applications/$ApplicationObjectId" -NoAuthCheck $true -AsApp $true -ErrorAction Stop + } else { + $Apps = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/applications?`$filter=appId eq '$AppId'&`$select=id,appId,identifierUris" -NoAuthCheck $true -AsApp $true -ErrorAction Stop + if ($Apps -is [array] -and $Apps.Count -gt 0) { + $Apps[0] + } elseif ($Apps.id) { + $Apps + } else { + throw "Application with AppId '$AppId' not found" + } + } + + $DesiredUri = "api://$($App.appId)" + $CurrentUris = @($App.identifierUris) + + Write-Information "Application '$($App.appId)': Current identifier URIs: $($CurrentUris -join ', ')" + + # Check if the desired URI is already present + if ($CurrentUris -contains $DesiredUri) { + return [PSCustomObject]@{ + Fixed = $false + PreviousUri = $CurrentUris -join ', ' + CurrentUri = $DesiredUri + Message = "Identifier URI '$DesiredUri' already correctly configured" + } + } + + # Need to add/fix the URI + Write-Information "Identifier URI missing or incorrect. Setting to '$DesiredUri'" + + if ($PSCmdlet.ShouldProcess($App.appId, "Set identifier URI to '$DesiredUri'")) { + $PatchBody = @{ + identifierUris = @($DesiredUri) + } + + $Retries = 0 + $MaxRetries = 3 + $Success = $false + + while (-not $Success -and $Retries -lt $MaxRetries) { + try { + $Retries++ + New-GraphPOSTRequest -uri "https://graph.microsoft.com/v1.0/applications/$($App.id)" -AsApp $true -NoAuthCheck $true -type PATCH -body $PatchBody -maxRetries 1 | Out-Null + $Success = $true + Write-Information "Successfully set identifier URI on attempt $Retries" + } catch { + $ErrorMsg = $_.Exception.Message + Write-Warning "Attempt $Retries to set identifier URI failed: $ErrorMsg" + + if ($Retries -lt $MaxRetries) { + Start-Sleep -Seconds 2 + } else { + throw "Failed to set identifier URI after $MaxRetries attempts: $ErrorMsg" + } + } + } + + return [PSCustomObject]@{ + Fixed = $true + PreviousUri = $CurrentUris -join ', ' + CurrentUri = $DesiredUri + Message = "Identifier URI successfully set to '$DesiredUri'" + } + } + } catch { + Write-Warning "Failed to repair identifier URI for AppId '$AppId': $($_.Exception.Message)" + throw + } +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecApiClient.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecApiClient.ps1 index 8315d00c1ca6..24eeb46c9b27 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecApiClient.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecApiClient.ps1 @@ -228,6 +228,40 @@ function Invoke-ExecApiClient { } $Body = @($Results) } + 'RepairUri' { + $Client = Get-CIPPAzDataTableEntity @Table -Filter "RowKey eq '$($Request.Body.ClientId)'" + if (!$Client) { + $Results = @{ + resultText = 'API client not found' + state = 'error' + } + } else { + try { + $RepairResult = Repair-CippApiIdentifierUri -AppId $Request.Body.ClientId + + if ($RepairResult.Fixed) { + Write-LogMessage -headers $Request.Headers -API 'ExecApiClient' -message "Repaired identifier URI for $($Client.AppName) $($RepairResult.Message)" -Sev 'Info' + $Results = @{ + resultText = "Identifier URI fixed for $($Client.AppName). $($RepairResult.Message)" + state = 'success' + } + } else { + $Results = @{ + resultText = "Identifier URI already correct for $($Client.AppName). $($RepairResult.Message)" + state = 'info' + } + } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -headers $Request.Headers -API 'ExecApiClient' -message "Failed to repair identifier URI for $($Client.AppName) $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage + $Results = @{ + resultText = "Failed to repair identifier URI for $($Client.AppName) $($ErrorMessage.NormalizedError)" + state = 'error' + } + } + } + $Body = @($Results) + } 'Delete' { try { if ($Request.Body.ClientId) { From 95d48d1fe90a2e57c7459afa1330290ef659ff02 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 27 May 2026 13:58:17 +0800 Subject: [PATCH 043/202] Fix for desktop activations copilot ready test --- .../Invoke-CippTestCopilotReady003.ps1 | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady003.ps1 b/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady003.ps1 index 898098726583..aa2fc5f723fa 100644 --- a/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady003.ps1 +++ b/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady003.ps1 @@ -46,19 +46,30 @@ function Invoke-CippTestCopilotReady003 { # For each licensed user, check if they have a desktop activation in the activation report. # Users absent from the report entirely are counted as unactivated. + # The Graph API returns activation counts nested inside userActivationCounts[] per product type, + # so we sum windows/mac across all product types to get the total desktop activation count. $NoDesktopUsers = [System.Collections.Generic.List[object]]::new() $DesktopCount = 0 foreach ($User in $LicensedUsers) { $Activation = $ActivationLookup[$User.userPrincipalName.ToLower()] - if ($Activation -and (([int]($Activation.windows ?? 0) + [int]($Activation.mac ?? 0)) -gt 0)) { + $TotalWindows = 0 + $TotalMac = 0 + $TotalAndroid = 0 + $TotalIos = 0 + if ($Activation.userActivationCounts) { + $TotalWindows = ($Activation.userActivationCounts | Measure-Object -Property windows -Sum).Sum + $TotalMac = ($Activation.userActivationCounts | Measure-Object -Property mac -Sum).Sum + $TotalAndroid = ($Activation.userActivationCounts | Measure-Object -Property android -Sum).Sum + $TotalIos = ($Activation.userActivationCounts | Measure-Object -Property ios -Sum).Sum + } + if ($Activation -and (($TotalWindows + $TotalMac) -gt 0)) { $DesktopCount++ } else { $NoDesktopUsers.Add([pscustomobject]@{ displayName = $User.displayName userPrincipalName = $User.userPrincipalName - web = if ($Activation) { $Activation.web } else { 0 } - android = if ($Activation) { $Activation.android } else { 0 } - ios = if ($Activation) { $Activation.ios } else { 0 } + android = if ($Activation) { $TotalAndroid } else { 0 } + ios = if ($Activation) { $TotalIos } else { 0 } neverActivated = ($null -eq $Activation) }) } @@ -83,7 +94,6 @@ function Invoke-CippTestCopilotReady003 { $PlatformStr = ' (never activated)' } else { $Platforms = @() - if ([int]($User.web ?? 0) -gt 0) { $Platforms += 'Web' } if ([int]($User.android ?? 0) -gt 0 -or [int]($User.ios ?? 0) -gt 0) { $Platforms += 'Mobile' } $PlatformStr = if ($Platforms) { " ($(($Platforms -join ', ')) only)" } else { ' (no activations)' } } @@ -93,7 +103,7 @@ function Invoke-CippTestCopilotReady003 { $NeverActivated = @($NoDesktopUsers | Where-Object { $_.neverActivated }).Count $Result += "**$($NoDesktopUsers.Count) users** have no desktop M365 Apps activation" if ($NeverActivated -gt 0) { $Result += " ($NeverActivated have never activated on any platform)" } - $Result += '.`n' + $Result += ".`n" } } From a6fdfe23eb387354bb0c1bd461e8d807e06dcee5 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 27 May 2026 13:59:27 +0800 Subject: [PATCH 044/202] Make all tenants list for SPO sites fast --- .../Get-CIPPSharePointSiteUsageReport.ps1 | 59 +++++++++++++++---- 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/Modules/CIPPCore/Public/Get-CIPPSharePointSiteUsageReport.ps1 b/Modules/CIPPCore/Public/Get-CIPPSharePointSiteUsageReport.ps1 index c4f76eaaa272..89e9eb3c196d 100644 --- a/Modules/CIPPCore/Public/Get-CIPPSharePointSiteUsageReport.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPSharePointSiteUsageReport.ps1 @@ -18,23 +18,56 @@ function Get-CIPPSharePointSiteUsageReport { try { if ($TenantFilter -eq 'AllTenants') { - $AllSiteItems = Get-CIPPDbItem -TenantFilter 'allTenants' -Type 'SharePointSiteListing' - $Tenants = @($AllSiteItems | Where-Object { $_.RowKey -ne 'SharePointSiteListing-Count' } | Select-Object -ExpandProperty PartitionKey -Unique) + # Bulk-fetch all site listings and usage data in 2 queries instead of per-tenant + $AllSiteItems = @(Get-CIPPDbItem -TenantFilter 'allTenants' -Type 'SharePointSiteListing' | Where-Object { $_.RowKey -ne 'SharePointSiteListing-Count' }) + $AllUsageItems = @(Get-CIPPDbItem -TenantFilter 'allTenants' -Type 'SharePointSiteUsage' | Where-Object { $_.RowKey -ne 'SharePointSiteUsage-Count' }) $TenantList = Get-Tenants -IncludeErrors - $Tenants = $Tenants | Where-Object { $TenantList.defaultDomainName -contains $_ } + $ValidTenants = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) + foreach ($T in $TenantList) { [void]$ValidTenants.Add($T.defaultDomainName) } + + # Build usage lookup keyed by siteId across all tenants + $UsageBySiteId = [System.Collections.Generic.Dictionary[string, object]]::new([System.StringComparer]::OrdinalIgnoreCase) + foreach ($UsageItem in $AllUsageItems) { + $UsageRow = $UsageItem.Data | ConvertFrom-Json -Depth 10 + if (-not [string]::IsNullOrWhiteSpace($UsageRow.siteId)) { + $UsageBySiteId[[string]$UsageRow.siteId] = $UsageRow + } + } $AllResults = [System.Collections.Generic.List[PSCustomObject]]::new() - foreach ($Tenant in $Tenants) { - try { - $TenantResults = Get-CIPPSharePointSiteUsageReport -TenantFilter $Tenant - foreach ($Result in $TenantResults) { - $Result | Add-Member -NotePropertyName 'Tenant' -NotePropertyValue $Tenant -Force - $AllResults.Add($Result) - } - } catch { - Write-LogMessage -API 'SharePointSiteUsageReport' -tenant $Tenant -message "Failed to get report for tenant: $($_.Exception.Message)" -sev Warning - } + foreach ($SiteItem in $AllSiteItems) { + $Tenant = $SiteItem.PartitionKey + if (-not $ValidTenants.Contains($Tenant)) { continue } + + $Site = $SiteItem.Data | ConvertFrom-Json -Depth 10 + if ($Site.isPersonalSite -eq $true) { continue } + + $SiteUsage = $null + [void]$UsageBySiteId.TryGetValue([string]$Site.sharepointIds.siteId, [ref]$SiteUsage) + + $StorageUsedInBytes = [double]($SiteUsage.storageUsedInBytes ?? 0) + $StorageAllocatedInBytes = [double]($SiteUsage.storageAllocatedInBytes ?? 0) + + $AllResults.Add([PSCustomObject]@{ + Tenant = $Tenant + siteId = $Site.sharepointIds.siteId + webId = $Site.sharepointIds.webId + createdDateTime = $Site.createdDateTime + displayName = $Site.displayName + webUrl = $Site.webUrl + ownerDisplayName = $SiteUsage.ownerDisplayName + ownerPrincipalName = $SiteUsage.ownerPrincipalName + lastActivityDate = $SiteUsage.lastActivityDate + fileCount = $SiteUsage.fileCount + storageUsedInGigabytes = [math]::round($StorageUsedInBytes / 1GB, 2) + storageAllocatedInGigabytes = [math]::round($StorageAllocatedInBytes / 1GB, 2) + storageUsedInBytes = $SiteUsage.storageUsedInBytes + storageAllocatedInBytes = $SiteUsage.storageAllocatedInBytes + rootWebTemplate = $SiteUsage.rootWebTemplate + reportRefreshDate = $SiteUsage.reportRefreshDate + AutoMapUrl = $Site.AutoMapUrl + }) } return $AllResults } From 122aec8f757ac9e96e515b19cd5965c7d8f3e5d3 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 27 May 2026 15:00:16 +0800 Subject: [PATCH 045/202] fix for template id casing --- .../HTTP Functions/Tenant/Standards/Invoke-ExecStandardsRun.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ExecStandardsRun.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ExecStandardsRun.ps1 index 0cd410faaa5a..70f8593e74f1 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ExecStandardsRun.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ExecStandardsRun.ps1 @@ -13,7 +13,7 @@ function Invoke-ExecStandardsRun { $TenantFilter = $Request.Query.tenantFilter ?? 'allTenants' - $TemplateId = $Request.Query.templateId ?? '*' + $TemplateId = $Request.Query.templateId ?? $Request.Query.TemplateId ?? '*' $Table = Get-CippTable -tablename 'templates' $Filter = "PartitionKey eq 'StandardsTemplateV2'" $Templates = (Get-CIPPAzDataTableEntity @Table -Filter $Filter | Sort-Object TimeStamp).JSON | ForEach-Object { From 7d3b480edb21d46cea9ae0d21196eab2c670b358 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 27 May 2026 16:29:11 +0800 Subject: [PATCH 046/202] Update Invoke-CIPPStandardDefenderCompliancePolicy.ps1 --- .../Invoke-CIPPStandardDefenderCompliancePolicy.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDefenderCompliancePolicy.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDefenderCompliancePolicy.ps1 index f46791387453..86c475fa28b5 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDefenderCompliancePolicy.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDefenderCompliancePolicy.ps1 @@ -29,9 +29,9 @@ function Invoke-CIPPStandardDefenderCompliancePolicy { {"type":"switch","name":"standards.DefenderCompliancePolicy.allowPartnerToCollectIosPersonalCertificateMetadata","label":"Collect personal certificate metadata from iOS","defaultValue":false} {"type":"switch","name":"standards.DefenderCompliancePolicy.ConnectMac","label":"Connect macOS devices to MDE","defaultValue":false} {"type":"switch","name":"standards.DefenderCompliancePolicy.macDeviceBlockedOnMissingPartnerData","label":"Block macOS if partner data unavailable","defaultValue":false} - {"type":"switch","name":"standards.DefenderCompliancePolicy.ConnectWindows","label":"Connect Windows 10.0.15063+ to MDE","defaultValue":false} + {"type":"switch","name":"standards.DefenderCompliancePolicy.ConnectWindows","label":"Connect Windows 10.0.15063+ to MDE (Note: enabling this forces 'Block Windows if partner data unavailable' to on)","defaultValue":false} {"type":"switch","name":"standards.DefenderCompliancePolicy.windowsMobileApplicationManagementEnabled","label":"Connect Windows (MAM)","defaultValue":false} - {"type":"switch","name":"standards.DefenderCompliancePolicy.windowsDeviceBlockedOnMissingPartnerData","label":"Block Windows if partner data unavailable","defaultValue":false} + {"type":"switch","name":"standards.DefenderCompliancePolicy.windowsDeviceBlockedOnMissingPartnerData","label":"Block Windows if partner data unavailable (Note: Microsoft enforces this to on when Connect Windows 10.0.15063+ to MDE is on)","defaultValue":false} {"type":"switch","name":"standards.DefenderCompliancePolicy.BlockunsupportedOS","label":"Block unsupported OS versions","defaultValue":false} {"type":"switch","name":"standards.DefenderCompliancePolicy.AllowMEMEnforceCompliance","label":"Allow MEM enforcement of compliance","defaultValue":false} IMPACT @@ -62,7 +62,7 @@ function Invoke-CIPPStandardDefenderCompliancePolicy { allowPartnerToCollectIOSPersonalApplicationMetadata = [bool]$Settings.ConnectIosCompliance androidDeviceBlockedOnMissingPartnerData = [bool]$Settings.androidDeviceBlockedOnMissingPartnerData iosDeviceBlockedOnMissingPartnerData = [bool]$Settings.iosDeviceBlockedOnMissingPartnerData - windowsDeviceBlockedOnMissingPartnerData = [bool]$Settings.windowsDeviceBlockedOnMissingPartnerData + windowsDeviceBlockedOnMissingPartnerData = if ([bool]$Settings.ConnectWindows) { $true } else { [bool]$Settings.windowsDeviceBlockedOnMissingPartnerData } macDeviceBlockedOnMissingPartnerData = [bool]$Settings.macDeviceBlockedOnMissingPartnerData androidMobileApplicationManagementEnabled = [bool]$Settings.ConnectAndroidCompliance iosMobileApplicationManagementEnabled = [bool]$Settings.appSync From 491530194c73d6fea5256666d3b46a93ce83a74c Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 27 May 2026 16:41:48 +0800 Subject: [PATCH 047/202] use top 500 to minimise requests --- Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertNewRiskyUsers.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertNewRiskyUsers.ps1 b/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertNewRiskyUsers.ps1 index 108bcb9f416f..46a8033f3c88 100644 --- a/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertNewRiskyUsers.ps1 +++ b/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertNewRiskyUsers.ps1 @@ -22,7 +22,7 @@ function Get-CIPPAlertNewRiskyUsers { $RiskyUsersDelta = (Get-CIPPAzDataTableEntity @Deltatable -Filter $Filter).delta | ConvertFrom-Json -ErrorAction SilentlyContinue # Get current risky users with more detailed information - $NewDelta = (New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/identityProtection/riskyUsers' -tenantid $TenantFilter) | Select-Object userPrincipalName, riskLevel, riskState, riskDetail, riskLastUpdatedDateTime, isProcessing, history + $NewDelta = (New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/identityProtection/riskyUsers?`$top=500' -tenantid $TenantFilter) | Select-Object userPrincipalName, riskLevel, riskState, riskDetail, riskLastUpdatedDateTime, isProcessing, history $NewDeltatoSave = $NewDelta | ConvertTo-Json -Depth 10 -Compress -ErrorAction SilentlyContinue | Out-String $DeltaEntity = @{ From c5b0e59221366cb68a8233590fbc38ee3e5321c6 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 27 May 2026 14:23:59 +0200 Subject: [PATCH 048/202] smart lockout standard --- .../Invoke-CIPPStandardSmartLockout.ps1 | 170 ++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSmartLockout.ps1 diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSmartLockout.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSmartLockout.ps1 new file mode 100644 index 000000000000..91ffd8babc82 --- /dev/null +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSmartLockout.ps1 @@ -0,0 +1,170 @@ +function Invoke-CIPPStandardSmartLockout { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) SmartLockout + .SYNOPSIS + (Label) Configure Entra ID Smart Lockout + .DESCRIPTION + (Helptext) **Requires Entra ID P1.** Configures the Entra ID Smart Lockout settings including lockout duration, lockout threshold, and on-premises integration mode. + (DocsDescription) Configures the Entra ID Smart Lockout policy which protects against brute-force password attacks. Smart Lockout locks out bad actors who try to guess user passwords or use brute-force methods. It recognizes sign-ins from valid users and treats them differently from attackers. Settings include lockout duration (seconds), lockout threshold (failed attempts before lockout), and on-premises password protection mode (Audit or Enforced). + .NOTES + CAT + Entra (AAD) Standards + TAG + "EIDSCAPR05" + "EIDSCAPR06" + ADDEDCOMPONENT + {"type":"number","name":"standards.SmartLockout.LockoutDurationInSeconds","label":"Lockout Duration (seconds)","default":60,"required":true} + {"type":"number","name":"standards.SmartLockout.LockoutThreshold","label":"Lockout Threshold (failed attempts)","default":10,"required":true} + {"type":"switch","name":"standards.SmartLockout.EnableBannedPasswordCheckOnPremises","label":"Enable On-Premises Password Protection"} + {"type":"radio","name":"standards.SmartLockout.BannedPasswordCheckOnPremisesMode","label":"On-Premises Mode","options":[{"label":"Audit","value":"Audit"},{"label":"Enforced","value":"Enforced"}]} + IMPACT + Medium Impact + ADDEDDATE + 2025-05-27 + POWERSHELLEQUIVALENT + Get-MgBetaDirectorySetting, New-MgBetaDirectorySetting, Update-MgBetaDirectorySetting + RECOMMENDEDBY + "CIS" + REQUIREDCAPABILITIES + "AAD_PREMIUM" + "AAD_PREMIUM_P2" + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + .LINK + https://docs.cipp.app/user-documentation/tenant/standards/alignment/templates/available-standards + #> + + param($Tenant, $Settings) + + $TestResult = Test-CIPPStandardLicense -StandardName 'SmartLockout' -TenantFilter $Tenant -Preset Entra + + if ($TestResult -eq $false) { + return $true + } + + $PasswordRuleTemplateId = '5cf42378-d67d-4f36-ba46-e8b86229381d' + + # Extract desired values from settings + $DesiredLockoutDuration = [string]($Settings.LockoutDurationInSeconds.value ?? $Settings.LockoutDurationInSeconds ?? '60') + $DesiredLockoutThreshold = [string]($Settings.LockoutThreshold.value ?? $Settings.LockoutThreshold ?? '10') + $DesiredEnableOnPrem = [string]($Settings.EnableBannedPasswordCheckOnPremises.value ?? $Settings.EnableBannedPasswordCheckOnPremises ?? 'False') + $DesiredOnPremMode = $Settings.BannedPasswordCheckOnPremisesMode.value ?? $Settings.BannedPasswordCheckOnPremisesMode ?? 'Audit' + + # Normalize boolean switch to string + if ($DesiredEnableOnPrem -eq $true -or $DesiredEnableOnPrem -eq 'true' -or $DesiredEnableOnPrem -eq 'True') { + $DesiredEnableOnPrem = 'True' + } else { + $DesiredEnableOnPrem = 'False' + } + + # Get existing directory settings for password rules + try { + $ExistingSettings = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/settings' -tenantid $Tenant | Where-Object { $_.templateId -eq $PasswordRuleTemplateId } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to get Smart Lockout settings: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + return + } + + # Extract current values + if ($null -ne $ExistingSettings) { + $CurrentLockoutDuration = ($ExistingSettings.values | Where-Object { $_.name -eq 'LockoutDurationInSeconds' }).value + $CurrentLockoutThreshold = ($ExistingSettings.values | Where-Object { $_.name -eq 'LockoutThreshold' }).value + $CurrentEnableOnPrem = ($ExistingSettings.values | Where-Object { $_.name -eq 'EnableBannedPasswordCheckOnPremises' }).value + $CurrentOnPremMode = ($ExistingSettings.values | Where-Object { $_.name -eq 'BannedPasswordCheckOnPremisesMode' }).value + } + + $StateIsCorrect = $null -ne $ExistingSettings -and + $CurrentLockoutDuration -eq $DesiredLockoutDuration -and + $CurrentLockoutThreshold -eq $DesiredLockoutThreshold -and + $CurrentEnableOnPrem -eq $DesiredEnableOnPrem -and + $CurrentOnPremMode -eq $DesiredOnPremMode + + if ($Settings.remediate -eq $true) { + if ($StateIsCorrect) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Smart Lockout is already configured correctly.' -sev Info + } else { + try { + if ($null -eq $ExistingSettings) { + # Create new directory setting with desired values + $Body = @{ + templateId = $PasswordRuleTemplateId + values = @( + @{ name = 'EnableBannedPasswordCheck'; value = 'False' } + @{ name = 'BannedPasswordList'; value = '' } + @{ name = 'LockoutDurationInSeconds'; value = $DesiredLockoutDuration } + @{ name = 'LockoutThreshold'; value = $DesiredLockoutThreshold } + @{ name = 'EnableBannedPasswordCheckOnPremises'; value = $DesiredEnableOnPrem } + @{ name = 'BannedPasswordCheckOnPremisesMode'; value = $DesiredOnPremMode } + ) + } + $JsonBody = ConvertTo-Json -Depth 10 -InputObject $Body -Compress + $null = New-GraphPostRequest -tenantid $Tenant -Uri 'https://graph.microsoft.com/beta/settings' -Type POST -Body $JsonBody + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Smart Lockout created: Duration=$DesiredLockoutDuration, Threshold=$DesiredLockoutThreshold, OnPrem=$DesiredEnableOnPrem, Mode=$DesiredOnPremMode" -sev Info + } else { + # Update existing directory setting, preserving banned password list values + $CurrentBannedPasswordCheck = ($ExistingSettings.values | Where-Object { $_.name -eq 'EnableBannedPasswordCheck' }).value + $CurrentBannedPasswordList = ($ExistingSettings.values | Where-Object { $_.name -eq 'BannedPasswordList' }).value + + $Body = @{ + values = @( + @{ name = 'EnableBannedPasswordCheck'; value = $CurrentBannedPasswordCheck } + @{ name = 'BannedPasswordList'; value = $CurrentBannedPasswordList } + @{ name = 'LockoutDurationInSeconds'; value = $DesiredLockoutDuration } + @{ name = 'LockoutThreshold'; value = $DesiredLockoutThreshold } + @{ name = 'EnableBannedPasswordCheckOnPremises'; value = $DesiredEnableOnPrem } + @{ name = 'BannedPasswordCheckOnPremisesMode'; value = $DesiredOnPremMode } + ) + } + $JsonBody = ConvertTo-Json -Depth 10 -InputObject $Body -Compress + $null = New-GraphPostRequest -tenantid $Tenant -Uri "https://graph.microsoft.com/beta/settings/$($ExistingSettings.id)" -Type PATCH -Body $JsonBody + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Smart Lockout updated: Duration=$DesiredLockoutDuration, Threshold=$DesiredLockoutThreshold, OnPrem=$DesiredEnableOnPrem, Mode=$DesiredOnPremMode" -sev Info + } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to configure Smart Lockout: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + } + } + } + + if ($Settings.alert -eq $true) { + if ($StateIsCorrect) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Smart Lockout is compliant.' -sev Info + } else { + $AlertObject = @{ + LockoutDurationInSeconds = $CurrentLockoutDuration ?? 'Not Configured' + LockoutThreshold = $CurrentLockoutThreshold ?? 'Not Configured' + EnableBannedPasswordCheckOnPremises = $CurrentEnableOnPrem ?? 'Not Configured' + BannedPasswordCheckOnPremisesMode = $CurrentOnPremMode ?? 'Not Configured' + DesiredLockoutDurationInSeconds = $DesiredLockoutDuration + DesiredLockoutThreshold = $DesiredLockoutThreshold + DesiredEnableOnPrem = $DesiredEnableOnPrem + DesiredOnPremMode = $DesiredOnPremMode + } + Write-StandardsAlert -message 'Smart Lockout is not configured correctly' -object $AlertObject -tenant $Tenant -standardName 'SmartLockout' -standardId $Settings.standardId + } + } + + if ($Settings.report -eq $true) { + $CurrentValue = @{ + LockoutDurationInSeconds = $CurrentLockoutDuration ?? 'Not Configured' + LockoutThreshold = $CurrentLockoutThreshold ?? 'Not Configured' + EnableBannedPasswordCheckOnPremises = $CurrentEnableOnPrem ?? 'Not Configured' + BannedPasswordCheckOnPremisesMode = $CurrentOnPremMode ?? 'Not Configured' + } + $ExpectedValue = @{ + LockoutDurationInSeconds = $DesiredLockoutDuration + LockoutThreshold = $DesiredLockoutThreshold + EnableBannedPasswordCheckOnPremises = $DesiredEnableOnPrem + BannedPasswordCheckOnPremisesMode = $DesiredOnPremMode + } + + Set-CIPPStandardsCompareField -FieldName 'standards.SmartLockout' ` + -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant + + Add-CIPPBPAField -FieldName 'SmartLockout' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant + } +} From c5a8a20739f1cc1265dbb6f0d8cf399e63df51b4 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 27 May 2026 14:24:02 +0200 Subject: [PATCH 049/202] smart lockout standard --- .../Invoke-CIPPStandardSmartLockout.ps1 | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSmartLockout.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSmartLockout.ps1 index 91ffd8babc82..8c6237bcc290 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSmartLockout.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSmartLockout.ps1 @@ -78,10 +78,10 @@ function Invoke-CIPPStandardSmartLockout { } $StateIsCorrect = $null -ne $ExistingSettings -and - $CurrentLockoutDuration -eq $DesiredLockoutDuration -and - $CurrentLockoutThreshold -eq $DesiredLockoutThreshold -and - $CurrentEnableOnPrem -eq $DesiredEnableOnPrem -and - $CurrentOnPremMode -eq $DesiredOnPremMode + $CurrentLockoutDuration -eq $DesiredLockoutDuration -and + $CurrentLockoutThreshold -eq $DesiredLockoutThreshold -and + $CurrentEnableOnPrem -eq $DesiredEnableOnPrem -and + $CurrentOnPremMode -eq $DesiredOnPremMode if ($Settings.remediate -eq $true) { if ($StateIsCorrect) { @@ -135,14 +135,14 @@ function Invoke-CIPPStandardSmartLockout { Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Smart Lockout is compliant.' -sev Info } else { $AlertObject = @{ - LockoutDurationInSeconds = $CurrentLockoutDuration ?? 'Not Configured' - LockoutThreshold = $CurrentLockoutThreshold ?? 'Not Configured' - EnableBannedPasswordCheckOnPremises = $CurrentEnableOnPrem ?? 'Not Configured' - BannedPasswordCheckOnPremisesMode = $CurrentOnPremMode ?? 'Not Configured' - DesiredLockoutDurationInSeconds = $DesiredLockoutDuration - DesiredLockoutThreshold = $DesiredLockoutThreshold - DesiredEnableOnPrem = $DesiredEnableOnPrem - DesiredOnPremMode = $DesiredOnPremMode + LockoutDurationInSeconds = $CurrentLockoutDuration ?? 'Not Configured' + LockoutThreshold = $CurrentLockoutThreshold ?? 'Not Configured' + EnableBannedPasswordCheckOnPremises = $CurrentEnableOnPrem ?? 'Not Configured' + BannedPasswordCheckOnPremisesMode = $CurrentOnPremMode ?? 'Not Configured' + DesiredLockoutDurationInSeconds = $DesiredLockoutDuration + DesiredLockoutThreshold = $DesiredLockoutThreshold + DesiredEnableOnPrem = $DesiredEnableOnPrem + DesiredOnPremMode = $DesiredOnPremMode } Write-StandardsAlert -message 'Smart Lockout is not configured correctly' -object $AlertObject -tenant $Tenant -standardName 'SmartLockout' -standardId $Settings.standardId } @@ -150,14 +150,14 @@ function Invoke-CIPPStandardSmartLockout { if ($Settings.report -eq $true) { $CurrentValue = @{ - LockoutDurationInSeconds = $CurrentLockoutDuration ?? 'Not Configured' - LockoutThreshold = $CurrentLockoutThreshold ?? 'Not Configured' + LockoutDurationInSeconds = $CurrentLockoutDuration ?? 'Not Configured' + LockoutThreshold = $CurrentLockoutThreshold ?? 'Not Configured' EnableBannedPasswordCheckOnPremises = $CurrentEnableOnPrem ?? 'Not Configured' BannedPasswordCheckOnPremisesMode = $CurrentOnPremMode ?? 'Not Configured' } $ExpectedValue = @{ - LockoutDurationInSeconds = $DesiredLockoutDuration - LockoutThreshold = $DesiredLockoutThreshold + LockoutDurationInSeconds = $DesiredLockoutDuration + LockoutThreshold = $DesiredLockoutThreshold EnableBannedPasswordCheckOnPremises = $DesiredEnableOnPrem BannedPasswordCheckOnPremisesMode = $DesiredOnPremMode } From f85963b72168e37d8820de7a7f469240dc968fd0 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 27 May 2026 17:03:21 +0200 Subject: [PATCH 050/202] Sharepoint management functionality. --- Modules/CIPPCore/Public/Get-CIPPSPOSite.ps1 | 62 +++++++ Modules/CIPPCore/Public/Set-CIPPSPOSite.ps1 | 75 +++++++++ Modules/CIPPCore/Public/Set-CIPPSPOTenant.ps1 | 96 +++++++---- .../Public/Start-CIPPSiteVersionCleanup.ps1 | 80 +++++++++ .../Invoke-CIPPStandardSPOVersionControl.ps1 | 153 ++++++++++++++++++ 5 files changed, 436 insertions(+), 30 deletions(-) create mode 100644 Modules/CIPPCore/Public/Get-CIPPSPOSite.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPSPOSite.ps1 create mode 100644 Modules/CIPPCore/Public/Start-CIPPSiteVersionCleanup.ps1 create mode 100644 Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPOVersionControl.ps1 diff --git a/Modules/CIPPCore/Public/Get-CIPPSPOSite.ps1 b/Modules/CIPPCore/Public/Get-CIPPSPOSite.ps1 new file mode 100644 index 000000000000..3f4a118c1f9e --- /dev/null +++ b/Modules/CIPPCore/Public/Get-CIPPSPOSite.ps1 @@ -0,0 +1,62 @@ +function Get-CIPPSPOSite { + <# + .SYNOPSIS + Get SharePoint Site properties via CSOM + + .DESCRIPTION + Retrieves all SharePoint site properties from the tenant using the CSOM GetSitePropertiesFromSharePoint method. + Returns all site properties including version policy settings. + + .PARAMETER TenantFilter + Tenant to query + + .EXAMPLE + Get-CIPPSPOSite -TenantFilter 'contoso.onmicrosoft.com' + + .FUNCTIONALITY + Internal + + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + $SharePointInfo = Get-SharePointAdminLink -Public $false -tenantFilter $TenantFilter + $AdminUrl = $SharePointInfo.AdminUrl + + $XML = @' +false +'@ + + $AdditionalHeaders = @{ + 'Accept' = 'application/json;odata=verbose' + } + + $AllSites = [System.Collections.Generic.List[object]]::new() + $StartIndex = $null + + do { + if ($null -ne $StartIndex) { + $XML = @" +$([System.Security.SecurityElement]::Escape($StartIndex))false +"@ + } + + $Results = New-GraphPostRequest -scope "$AdminUrl/.default" -tenantid $TenantFilter -Uri "$AdminUrl/_vti_bin/client.svc/ProcessQuery" -Type POST -Body $XML -ContentType 'text/xml' -AddedHeaders $AdditionalHeaders + + # The response contains multiple objects; find the one with _Child_Items_ (site list) and NextStartIndexFromSharePoint + $SiteCollection = $Results | Where-Object { $_._Child_Items_ } + if ($SiteCollection) { + foreach ($Site in $SiteCollection._Child_Items_) { + [void]$AllSites.Add($Site) + } + $StartIndex = $SiteCollection.NextStartIndexFromSharePoint + } else { + $StartIndex = $null + } + } while ($StartIndex) + + return $AllSites +} diff --git a/Modules/CIPPCore/Public/Set-CIPPSPOSite.ps1 b/Modules/CIPPCore/Public/Set-CIPPSPOSite.ps1 new file mode 100644 index 000000000000..4af8e4f6b55a --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPSPOSite.ps1 @@ -0,0 +1,75 @@ +function Set-CIPPSPOSite { + <# + .SYNOPSIS + Set SharePoint Site properties via CSOM + + .DESCRIPTION + Sets properties on an individual SharePoint site using the CSOM GetSitePropertiesByUrl + SetProperty + Update pattern. + + .PARAMETER TenantFilter + Tenant to apply settings to + + .PARAMETER SiteUrl + Full URL of the SharePoint site to modify + + .PARAMETER Properties + Hashtable of site properties to change. Supported value types: Boolean, String, Int32. + + .EXAMPLE + $Properties = @{ + InheritVersionPolicyFromTenant = $false + EnableAutoExpirationVersionTrim = $false + ApplyToNewDocumentLibraries = $true + ApplyToExistingDocumentLibraries = $true + MajorVersionLimit = 50 + ExpireVersionsAfterDays = 365 + } + Set-CIPPSPOSite -TenantFilter 'contoso.onmicrosoft.com' -SiteUrl 'https://contoso.sharepoint.com/sites/MySite' -Properties $Properties + + .FUNCTIONALITY + Internal + + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter, + [Parameter(Mandatory = $true)] + [string]$SiteUrl, + [Parameter(Mandatory = $true)] + [hashtable]$Properties + ) + + $SharePointInfo = Get-SharePointAdminLink -Public $false -tenantFilter $TenantFilter + $AdminUrl = $SharePointInfo.AdminUrl + + $AllowedTypes = @('Boolean', 'String', 'Int32') + $SetProperty = [System.Collections.Generic.List[string]]::new() + $x = 106 + foreach ($Property in $Properties.Keys) { + $PropertyType = $Properties[$Property].GetType().Name + if ($PropertyType -in $AllowedTypes) { + $PropertyToSet = if ($PropertyType -eq 'Boolean') { $Properties[$Property].ToString().ToLower() } else { $Properties[$Property] } + $SetProperty.Add("$PropertyToSet") + $x++ + } + } + + if ($SetProperty.Count -eq 0) { + Write-Error 'No valid properties found' + return + } + + # CSOM pattern: Tenant Constructor → GetSitePropertiesByUrl → SetProperty(s) → Update + $XML = @" +$($SetProperty -join '')$([System.Security.SecurityElement]::Escape($SiteUrl))false +"@ + + $AdditionalHeaders = @{ + 'Accept' = 'application/json;odata=verbose' + } + + if ($PSCmdlet.ShouldProcess($SiteUrl, 'Set Site Properties')) { + New-GraphPostRequest -scope "$AdminUrl/.default" -tenantid $TenantFilter -Uri "$AdminUrl/_vti_bin/client.svc/ProcessQuery" -Type POST -Body $XML -ContentType 'text/xml' -AddedHeaders $AdditionalHeaders + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPSPOTenant.ps1 b/Modules/CIPPCore/Public/Set-CIPPSPOTenant.ps1 index ea3e9ded3851..a7d79797147f 100644 --- a/Modules/CIPPCore/Public/Set-CIPPSPOTenant.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPSPOTenant.ps1 @@ -1,10 +1,10 @@ function Set-CIPPSPOTenant { <# .SYNOPSIS - Set SharePoint Tenant properties + Set SharePoint Tenant properties or invoke methods .DESCRIPTION - Set SharePoint Tenant properties via SPO API + Set SharePoint Tenant properties via SPO CSOM API, or invoke a CSOM method on the Tenant object. .PARAMETER TenantFilter Tenant to apply settings to @@ -13,7 +13,14 @@ function Set-CIPPSPOTenant { Tenant Identity (Get from Get-CIPPSPOTenant) .PARAMETER Properties - Hashtable of tenant properties to change + Hashtable of tenant properties to change (uses SetProperty actions) + + .PARAMETER MethodName + Name of the CSOM method to invoke on the Tenant object + + .PARAMETER MethodParameters + Ordered array of parameter hashtables for the method call. Each entry must have 'Type' and 'Value' keys. + Supported types: Boolean, String, Int32, Int64. .PARAMETER SharepointPrefix Prefix for the sharepoint tenant @@ -24,20 +31,35 @@ function Set-CIPPSPOTenant { } Get-CippSPOTenant -TenantFilter 'contoso.onmicrosoft.com' | Set-CIPPSPOTenant -Properties $Properties + .EXAMPLE + $MethodParams = @( + @{ Type = 'Boolean'; Value = $false } + @{ Type = 'Int32'; Value = 50 } + @{ Type = 'Int32'; Value = 365 } + ) + Get-CIPPSPOTenant -TenantFilter 'contoso.onmicrosoft.com' | Set-CIPPSPOTenant -MethodName 'SetFileVersionPolicy' -MethodParameters $MethodParams + .FUNCTIONALITY Internal #> - [CmdletBinding(SupportsShouldProcess = $true)] + [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = 'Properties')] param( - [Parameter(ValueFromPipelineByPropertyName = $true, Mandatory = $true)] + [Parameter(ValueFromPipelineByPropertyName = $true, Mandatory = $true, ParameterSetName = 'Properties')] + [Parameter(ValueFromPipelineByPropertyName = $true, Mandatory = $true, ParameterSetName = 'Method')] [string]$TenantFilter, - [Parameter(ValueFromPipelineByPropertyName = $true, Mandatory = $true)] + [Parameter(ValueFromPipelineByPropertyName = $true, Mandatory = $true, ParameterSetName = 'Properties')] + [Parameter(ValueFromPipelineByPropertyName = $true, Mandatory = $true, ParameterSetName = 'Method')] [Alias('_ObjectIdentity_')] [string]$Identity, - [Parameter(Mandatory = $true)] + [Parameter(Mandatory = $true, ParameterSetName = 'Properties')] [hashtable]$Properties, - [Parameter(ValueFromPipelineByPropertyName = $true)] + [Parameter(Mandatory = $true, ParameterSetName = 'Method')] + [string]$MethodName, + [Parameter(Mandatory = $true, ParameterSetName = 'Method')] + [array]$MethodParameters, + [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Properties')] + [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Method')] [string]$SharepointPrefix ) @@ -51,42 +73,56 @@ function Set-CIPPSPOTenant { $AdminUrl = "https://$($tenantName)-admin.sharepoint.com" } $Identity = $Identity -replace "`n", ' ' - $AllowedTypes = @('Boolean', 'String', 'Int32') - $SetProperty = [System.Collections.Generic.List[string]]::new() - $x = 114 - foreach ($Property in $Properties.Keys) { - # Get property type - $PropertyType = $Properties[$Property].GetType().Name - if ($PropertyType -in $AllowedTypes) { - if ($PropertyType -eq 'Boolean') { - $PropertyToSet = $Properties[$Property].ToString().ToLower() - } else { - $PropertyToSet = $Properties[$Property] - } - $xml = @" + + if ($PSCmdlet.ParameterSetName -eq 'Properties') { + $AllowedTypes = @('Boolean', 'String', 'Int32') + $SetProperty = [System.Collections.Generic.List[string]]::new() + $x = 114 + foreach ($Property in $Properties.Keys) { + # Get property type + $PropertyType = $Properties[$Property].GetType().Name + if ($PropertyType -in $AllowedTypes) { + if ($PropertyType -eq 'Boolean') { + $PropertyToSet = $Properties[$Property].ToString().ToLower() + } else { + $PropertyToSet = $Properties[$Property] + } + $xml = @" $($PropertyToSet) "@ - $SetProperty.Add($xml) - $x++ + $SetProperty.Add($xml) + $x++ + } + } + + if (($SetProperty | Measure-Object).Count -eq 0) { + Write-Error 'No valid properties found' + return } - } - if (($SetProperty | Measure-Object).Count -eq 0) { - Write-Error 'No valid properties found' - return + $ActionsXml = $SetProperty -join '' + $Description = $Properties.Keys -join ', ' + } else { + # Method call + $Params = foreach ($Param in $MethodParameters) { + $ParamValue = if ($Param.Type -eq 'Boolean') { $Param.Value.ToString().ToLower() } else { $Param.Value } + "$ParamValue" + } + $ActionsXml = "$($Params -join '')" + $Description = $MethodName } - # Query tenant settings + # Build CSOM request $XML = @" - $($SetProperty -join '') + $ActionsXml "@ $AdditionalHeaders = @{ 'Accept' = 'application/json;odata=verbose' } - if ($PSCmdlet.ShouldProcess(($Properties.Keys -join ', '), 'Set Tenant Properties')) { + if ($PSCmdlet.ShouldProcess($Description, 'Set Tenant Properties')) { New-GraphPostRequest -scope "$AdminURL/.default" -tenantid $TenantFilter -Uri "$AdminURL/_vti_bin/client.svc/ProcessQuery" -Type POST -Body $XML -ContentType 'text/xml' -AddedHeaders $AdditionalHeaders # Invalidate cached tenant data so subsequent reads reflect the change diff --git a/Modules/CIPPCore/Public/Start-CIPPSiteVersionCleanup.ps1 b/Modules/CIPPCore/Public/Start-CIPPSiteVersionCleanup.ps1 new file mode 100644 index 000000000000..ef9e032aa9f6 --- /dev/null +++ b/Modules/CIPPCore/Public/Start-CIPPSiteVersionCleanup.ps1 @@ -0,0 +1,80 @@ +function Start-CIPPSiteVersionCleanup { + <# + .SYNOPSIS + Start a file version batch delete job for a SharePoint site + + .DESCRIPTION + Creates a new file version batch delete job via the CSOM NewFileVersionBatchDeleteJob method. + This triggers cleanup of old file versions on a SharePoint site based on the specified parameters. + + .PARAMETER TenantFilter + Tenant to run the cleanup on + + .PARAMETER SiteUrl + Full URL of the SharePoint site to clean up + + .PARAMETER BatchDeleteMode + Cleanup mode as an enum value: + 0 = DeleteOlderThanDays + 1 = CountLimits + 2 = SyncPolicy (apply the site's current version policy) + + .PARAMETER DeleteOlderThanDays + Delete versions older than this many days. Use -1 to skip (when using SyncPolicy mode). + + .PARAMETER MajorVersionLimit + Maximum major versions to keep. Use -1 to skip (when using SyncPolicy mode). + + .PARAMETER MajorWithMinorVersionsLimit + Maximum major versions that retain minor versions. Use -1 to skip (when using SyncPolicy mode). + + .PARAMETER SyncListPolicy + Whether to sync the list-level policy. Defaults to $false. + + .EXAMPLE + # Sync site policy (apply current version settings to all existing versions) + Start-CIPPSiteVersionCleanup -TenantFilter 'contoso.onmicrosoft.com' -SiteUrl 'https://contoso.sharepoint.com/sites/MySite' -BatchDeleteMode 2 + + .EXAMPLE + # Delete versions older than 365 days + Start-CIPPSiteVersionCleanup -TenantFilter 'contoso.onmicrosoft.com' -SiteUrl 'https://contoso.sharepoint.com/sites/MySite' -BatchDeleteMode 0 -DeleteOlderThanDays 365 + + .FUNCTIONALITY + Internal + + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter, + [Parameter(Mandatory = $true)] + [string]$SiteUrl, + [Parameter(Mandatory = $false)] + [int]$BatchDeleteMode = 2, + [Parameter(Mandatory = $false)] + [int]$DeleteOlderThanDays = -1, + [Parameter(Mandatory = $false)] + [int]$MajorVersionLimit = -1, + [Parameter(Mandatory = $false)] + [int]$MajorWithMinorVersionsLimit = -1, + [Parameter(Mandatory = $false)] + [bool]$SyncListPolicy = $false + ) + + $SharePointInfo = Get-SharePointAdminLink -Public $false -tenantFilter $TenantFilter + $AdminUrl = $SharePointInfo.AdminUrl + $EscapedSiteUrl = [System.Security.SecurityElement]::Escape($SiteUrl) + $SyncListPolicyValue = $SyncListPolicy.ToString().ToLower() + + $XML = @" +$EscapedSiteUrl$BatchDeleteMode$DeleteOlderThanDays$MajorVersionLimit$MajorWithMinorVersionsLimit$SyncListPolicyValue +"@ + + $AdditionalHeaders = @{ + 'Accept' = 'application/json;odata=verbose' + } + + if ($PSCmdlet.ShouldProcess($SiteUrl, 'Start file version batch delete job')) { + return New-GraphPostRequest -scope "$AdminUrl/.default" -tenantid $TenantFilter -Uri "$AdminUrl/_vti_bin/client.svc/ProcessQuery" -Type POST -Body $XML -ContentType 'text/xml' -AddedHeaders $AdditionalHeaders + } +} diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPOVersionControl.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPOVersionControl.ps1 new file mode 100644 index 000000000000..fec828fc82ad --- /dev/null +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPOVersionControl.ps1 @@ -0,0 +1,153 @@ +function Invoke-CIPPStandardSPOVersionControl { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) SPOVersionControl + .SYNOPSIS + (Label) Set SharePoint File Version Limits + .DESCRIPTION + (Helptext) Configures SharePoint Online file versioning to either use automatic version trimming managed by Microsoft, or enforce a fixed major version limit with optional version expiration. + (DocsDescription) Configures the SharePoint Online tenant-level file versioning policy. When automatic version trimming is enabled, Microsoft intelligently manages version cleanup. When disabled, you can set a fixed maximum number of major versions to retain and optionally expire versions after a specified number of days. This helps manage storage consumption while preserving version history as needed. + .NOTES + CAT + SharePoint Standards + TAG + EXECUTIVETEXT + Controls how SharePoint Online manages file version history at the tenant level. Automatic trimming lets Microsoft optimize storage by cleaning up old versions intelligently. Manual limits give administrators precise control over the maximum number of versions retained and their expiration, ensuring predictable storage usage and compliance with data retention policies. + ADDEDCOMPONENT + {"type":"switch","name":"standards.SPOVersionControl.EnableAutoTrim","label":"Enable Automatic Version Trimming (Microsoft managed)"} + {"type":"number","name":"standards.SPOVersionControl.MajorVersionLimit","label":"Maximum Major Versions (when auto trim is off)","default":50} + {"type":"number","name":"standards.SPOVersionControl.ExpireVersionsAfterDays","label":"Expire Versions After Days (0 = never, when auto trim is off)","default":0} + {"type":"switch","name":"standards.SPOVersionControl.ApplyToExistingSites","label":"Apply to all existing sites and document libraries"} + IMPACT + Medium Impact + ADDEDDATE + 2026-05-27 + POWERSHELLEQUIVALENT + Set-SPOTenant -EnableAutoExpirationVersionTrim $true or Set-SPOTenant -EnableAutoExpirationVersionTrim $false -MajorVersionLimit 50 -ExpireVersionsAfterDays 365 + RECOMMENDEDBY + "CIPP" + REQUIREDCAPABILITIES + "SHAREPOINTWAC" + "SHAREPOINTSTANDARD" + "SHAREPOINTENTERPRISE" + "SHAREPOINTENTERPRISE_EDU" + "SHAREPOINTENTERPRISE_GOV" + "ONEDRIVE_BASIC" + "ONEDRIVE_ENTERPRISE" + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + .LINK + https://docs.cipp.app/user-documentation/tenant/standards/alignment/templates/available-standards + #> + + param($Tenant, $Settings) + $TestResult = Test-CIPPStandardLicense -StandardName 'SPOVersionControl' -TenantFilter $Tenant -Preset SharePoint + + if ($TestResult -eq $false) { + return $true + } + + $DesiredAutoTrim = [bool]$Settings.EnableAutoTrim + $DesiredMajorVersionLimit = [int]($Settings.MajorVersionLimit ?? 50) + $DesiredExpireVersionsAfterDays = [int]($Settings.ExpireVersionsAfterDays ?? 0) + + try { + $CurrentState = Get-CIPPSPOTenant -TenantFilter $Tenant | Select-Object -Property _ObjectIdentity_, TenantFilter, EnableAutoExpirationVersionTrim, MajorVersionLimit, ExpireVersionsAfterDays + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -Tenant $Tenant -message "Could not get the SPOVersionControl state for $Tenant. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + return + } + + if ($DesiredAutoTrim) { + $StateIsCorrect = $CurrentState.EnableAutoExpirationVersionTrim -eq $true + } else { + $StateIsCorrect = ($CurrentState.EnableAutoExpirationVersionTrim -eq $false) -and + ($CurrentState.MajorVersionLimit -eq $DesiredMajorVersionLimit) -and + ($CurrentState.ExpireVersionsAfterDays -eq $DesiredExpireVersionsAfterDays) + } + + $CurrentValue = [PSCustomObject]@{ + EnableAutoExpirationVersionTrim = $CurrentState.EnableAutoExpirationVersionTrim + MajorVersionLimit = $CurrentState.MajorVersionLimit + ExpireVersionsAfterDays = $CurrentState.ExpireVersionsAfterDays + } + $ExpectedValue = [PSCustomObject]@{ + EnableAutoExpirationVersionTrim = $DesiredAutoTrim + MajorVersionLimit = if ($DesiredAutoTrim) { $null } else { $DesiredMajorVersionLimit } + ExpireVersionsAfterDays = if ($DesiredAutoTrim) { $null } else { $DesiredExpireVersionsAfterDays } + } + + if ($Settings.remediate -eq $true) { + if ($StateIsCorrect) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'SharePoint version control settings are already configured correctly' -sev Info + } else { + try { + # SetFileVersionPolicy method: params are (Boolean isAutoTrimEnabled, Int32 majorVersionLimit, Int32 expireVersionsAfterDays) + # When auto trim is on, pass -1 for the version/expiry params + if ($DesiredAutoTrim) { + $MethodParams = @( + @{ Type = 'Boolean'; Value = $true } + @{ Type = 'Int32'; Value = -1 } + @{ Type = 'Int32'; Value = -1 } + ) + } else { + $MethodParams = @( + @{ Type = 'Boolean'; Value = $false } + @{ Type = 'Int32'; Value = $DesiredMajorVersionLimit } + @{ Type = 'Int32'; Value = $DesiredExpireVersionsAfterDays } + ) + } + $CurrentState | Set-CIPPSPOTenant -MethodName 'SetFileVersionPolicy' -MethodParameters $MethodParams + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully configured SharePoint version control (AutoTrim: $DesiredAutoTrim, MajorVersionLimit: $DesiredMajorVersionLimit, ExpireVersionsAfterDays: $DesiredExpireVersionsAfterDays)" -sev Info + + # Apply to all existing sites and their document libraries + if ($Settings.ApplyToExistingSites -eq $true) { + $Sites = @(New-GraphGetRequest -uri "https://graph.microsoft.com/beta/sites/getAllSites?`$select=webUrl&`$top=999" -tenantid $Tenant -AsApp $true) + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Applying version policy to $($Sites.Count) existing sites" -sev Info + + $SiteProperties = @{ + InheritVersionPolicyFromTenant = $true + EnableAutoExpirationVersionTrim = $DesiredAutoTrim + ApplyToNewDocumentLibraries = $true + ApplyToExistingDocumentLibraries = $true + } + if (-not $DesiredAutoTrim) { + $SiteProperties.MajorVersionLimit = $DesiredMajorVersionLimit + $SiteProperties.ExpireVersionsAfterDays = $DesiredExpireVersionsAfterDays + } + + foreach ($Site in $Sites) { + try { + Set-CIPPSPOSite -TenantFilter $Tenant -SiteUrl $Site.webUrl -Properties $SiteProperties + } catch { + write-host "Failed to set version policy for site $($Site.webUrl). Exception" -ForegroundColor Red + $SiteError = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to set version policy for site $($Site.webUrl): $($SiteError.NormalizedError)" -sev Error -LogData $SiteError + } + } + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Finished applying version policy to existing sites" -sev Info + } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to set SharePoint version control settings. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + } + } + } + + if ($Settings.alert -eq $true) { + if ($StateIsCorrect) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'SharePoint version control settings are configured correctly' -sev Info + } else { + $Message = "SharePoint version control is not configured correctly. Current: AutoTrim=$($CurrentState.EnableAutoExpirationVersionTrim), MajorVersionLimit=$($CurrentState.MajorVersionLimit), ExpireVersionsAfterDays=$($CurrentState.ExpireVersionsAfterDays). Expected: AutoTrim=$DesiredAutoTrim, MajorVersionLimit=$DesiredMajorVersionLimit, ExpireVersionsAfterDays=$DesiredExpireVersionsAfterDays" + Write-StandardsAlert -message $Message -object $CurrentState -tenant $Tenant -standardName 'SPOVersionControl' -standardId $Settings.standardId + } + } + + if ($Settings.report -eq $true) { + Set-CIPPStandardsCompareField -FieldName 'standards.SPOVersionControl' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant + Add-CIPPBPAField -FieldName 'SPOVersionControl' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant + } +} From b7d4f5e2ca653947ec0ffb8f6403f1ba7407eb6b Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 27 May 2026 17:03:26 +0200 Subject: [PATCH 051/202] Sharepoint management functionality. --- .../Standards/Invoke-CIPPStandardSPOVersionControl.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPOVersionControl.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPOVersionControl.ps1 index fec828fc82ad..816f79cee071 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPOVersionControl.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPOVersionControl.ps1 @@ -54,7 +54,7 @@ function Invoke-CIPPStandardSPOVersionControl { $DesiredExpireVersionsAfterDays = [int]($Settings.ExpireVersionsAfterDays ?? 0) try { - $CurrentState = Get-CIPPSPOTenant -TenantFilter $Tenant | Select-Object -Property _ObjectIdentity_, TenantFilter, EnableAutoExpirationVersionTrim, MajorVersionLimit, ExpireVersionsAfterDays + $CurrentState = Get-CIPPSPOTenant -TenantFilter $Tenant | Select-Object -Property _ObjectIdentity_, TenantFilter, EnableAutoExpirationVersionTrim, MajorVersionLimit, ExpireVersionsAfterDays } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Standards' -Tenant $Tenant -message "Could not get the SPOVersionControl state for $Tenant. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage @@ -123,12 +123,12 @@ function Invoke-CIPPStandardSPOVersionControl { try { Set-CIPPSPOSite -TenantFilter $Tenant -SiteUrl $Site.webUrl -Properties $SiteProperties } catch { - write-host "Failed to set version policy for site $($Site.webUrl). Exception" -ForegroundColor Red + Write-Host "Failed to set version policy for site $($Site.webUrl). Exception" -ForegroundColor Red $SiteError = Get-CippException -Exception $_ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to set version policy for site $($Site.webUrl): $($SiteError.NormalizedError)" -sev Error -LogData $SiteError } } - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Finished applying version policy to existing sites" -sev Info + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Finished applying version policy to existing sites' -sev Info } } catch { $ErrorMessage = Get-CippException -Exception $_ From b7c72180ac5963a3c41211bc2a14e1a9a6bce796 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Wed, 27 May 2026 17:11:35 +0200 Subject: [PATCH 052/202] fix: update terminology from "Temporary Access Password" to "Temporary Access Pass" Fixes #6060 --- Config/standards.json | 107 ++++++++++++++++++------ Modules/CIPPCore/Public/New-CIPPTAP.ps1 | 4 +- 2 files changed, 84 insertions(+), 27 deletions(-) diff --git a/Config/standards.json b/Config/standards.json index ce9752a56d19..1e2ceba142dc 100644 --- a/Config/standards.json +++ b/Config/standards.json @@ -344,7 +344,12 @@ { "name": "standards.DisableBasicAuthSMTP", "cat": "Global Standards", - "tag": ["CIS M365 5.0 (6.5.4)", "NIST CSF 2.0 (PR.IR-01)", "ZTNA21799", "CISAMSEXO51"], + "tag": [ + "CIS M365 5.0 (6.5.4)", + "NIST CSF 2.0 (PR.IR-01)", + "ZTNA21799", + "CISAMSEXO51" + ], "helpText": "Disables SMTP AUTH organization-wide, impacting POP and IMAP clients that rely on SMTP for sending emails. Default for new tenants. For more information, see the [Microsoft documentation](https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/authenticated-client-smtp-submission)", "docsDescription": "Disables tenant-wide SMTP basic authentication, including for all explicitly enabled users, impacting POP and IMAP clients that rely on SMTP for sending emails. For more information, see the [Microsoft documentation](https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/authenticated-client-smtp-submission).", "executiveText": "Disables outdated email authentication methods that are vulnerable to security attacks, forcing applications and devices to use modern, more secure authentication protocols. This reduces the risk of email-based security breaches and credential theft.", @@ -410,7 +415,13 @@ { "name": "standards.AuthMethodsSettings", "cat": "Entra (AAD) Standards", - "tag": ["EIDSCA.AG01", "EIDSCA.AG02", "EIDSCA.AG03", "EIDSCAAG02", "EIDSCAAG03"], + "tag": [ + "EIDSCA.AG01", + "EIDSCA.AG02", + "EIDSCA.AG03", + "EIDSCAAG02", + "EIDSCAAG03" + ], "helpText": "Configures the report suspicious activity settings and system credential preferences in the authentication methods policy.", "docsDescription": "Controls the authentication methods policy settings for reporting suspicious activity and system credential preferences. These settings help enhance the security of authentication in your organization.", "executiveText": "Configures security settings that allow users to report suspicious login attempts and manages how the system handles authentication credentials. This enhances overall security by enabling early detection of potential security threats and optimizing authentication processes.", @@ -724,7 +735,7 @@ "tag": ["ZTNA21845", "ZTNA21846", "EIDSCAAT01", "EIDSCAAT02"], "helpText": "Enables TAP and sets the default TAP lifetime to 1 hour. This configuration also allows you to select if a TAP is single use or multi-logon.", "docsDescription": "Enables Temporary Password generation for the tenant.", - "executiveText": "Enables temporary access passwords that IT administrators can generate for employees who are locked out or need emergency access to systems. These time-limited passwords provide a secure way to restore access without compromising long-term security policies.", + "executiveText": "Enables temporary access passes that IT administrators can generate for employees who are locked out or need emergency access to systems. These time-limited passwords provide a secure way to restore access without compromising long-term security policies.", "addedComponent": [ { "type": "autoComplete", @@ -744,7 +755,7 @@ ] } ], - "label": "Enable Temporary Access Passwords", + "label": "Enable Temporary Access Passes", "impact": "Low Impact", "impactColour": "info", "addedDate": "2022-03-15", @@ -833,7 +844,12 @@ { "name": "standards.DisableTenantCreation", "cat": "Entra (AAD) Standards", - "tag": ["CIS M365 5.0 (1.2.3)", "CISA (MS.AAD.6.1v1)", "ZTNA21772", "ZTNA21787"], + "tag": [ + "CIS M365 5.0 (1.2.3)", + "CISA (MS.AAD.6.1v1)", + "ZTNA21772", + "ZTNA21787" + ], "helpText": "Restricts creation of M365 tenants to the Global Administrator or Tenant Creator roles.", "docsDescription": "Users by default are allowed to create M365 tenants. This disables that so only admins can create new M365 tenants.", "executiveText": "Prevents regular employees from creating new Microsoft 365 organizations, ensuring all new tenants are properly managed and controlled by IT administrators. This prevents unauthorized shadow IT environments and maintains centralized governance over Microsoft 365 resources.", @@ -1160,7 +1176,11 @@ { "name": "standards.StaleEntraDevices", "cat": "Entra (AAD) Standards", - "tag": ["Essential 8 (1501)", "NIST CSF 2.0 (ID.AM-08)", "NIST CSF 2.0 (PR.PS-03)"], + "tag": [ + "Essential 8 (1501)", + "NIST CSF 2.0 (ID.AM-08)", + "NIST CSF 2.0 (PR.PS-03)" + ], "helpText": "**Remediate is currently not available**. Cleans up Entra devices that have not connected/signed in for the specified number of days.", "docsDescription": "Remediate is currently not available. Cleans up Entra devices that have not connected/signed in for the specified number of days. First disables and later deletes the devices. More info can be found in the [Microsoft documentation](https://learn.microsoft.com/en-us/entra/identity/devices/manage-stale-devices)", "executiveText": "Automatically identifies and removes inactive devices that haven't connected to company systems for a specified period, reducing security risks from abandoned or lost devices. This maintains a clean device inventory and prevents potential unauthorized access through dormant device registrations.", @@ -1218,7 +1238,12 @@ { "name": "standards.DisableSMS", "cat": "Entra (AAD) Standards", - "tag": ["CIS M365 5.0 (2.3.5)", "EIDSCA.AS04", "NIST CSF 2.0 (PR.AA-03)", "EIDSCAAS04"], + "tag": [ + "CIS M365 5.0 (2.3.5)", + "EIDSCA.AS04", + "NIST CSF 2.0 (PR.AA-03)", + "EIDSCAAS04" + ], "helpText": "This blocks users from using SMS as an MFA method. If a user only has SMS as a MFA method, they will be unable to log in.", "docsDescription": "Disables SMS as an MFA method for the tenant. If a user only has SMS as a MFA method, they will be unable to sign in.", "executiveText": "Disables SMS text messages as a multi-factor authentication method due to security vulnerabilities like SIM swapping attacks. This forces users to adopt more secure authentication methods like authenticator apps or hardware tokens, significantly improving account security.", @@ -1233,7 +1258,12 @@ { "name": "standards.DisableVoice", "cat": "Entra (AAD) Standards", - "tag": ["CIS M365 5.0 (2.3.5)", "EIDSCA.AV01", "NIST CSF 2.0 (PR.AA-03)", "EIDSCAAV01"], + "tag": [ + "CIS M365 5.0 (2.3.5)", + "EIDSCA.AV01", + "NIST CSF 2.0 (PR.AA-03)", + "EIDSCAAV01" + ], "helpText": "This blocks users from using Voice call as an MFA method. If a user only has Voice as a MFA method, they will be unable to log in.", "docsDescription": "Disables Voice call as an MFA method for the tenant. If a user only has Voice call as a MFA method, they will be unable to sign in.", "executiveText": "Disables voice call authentication due to security vulnerabilities and social engineering risks. This forces users to adopt more secure authentication methods like authenticator apps, improving overall account security by eliminating phone-based attack vectors.", @@ -2071,7 +2101,12 @@ { "name": "standards.DisableExternalCalendarSharing", "cat": "Exchange Standards", - "tag": ["CIS M365 5.0 (1.3.3)", "exo_individualsharing", "ZTNA21803", "CISAMSEXO62"], + "tag": [ + "CIS M365 5.0 (1.3.3)", + "exo_individualsharing", + "ZTNA21803", + "CISAMSEXO62" + ], "helpText": "Disables the ability for users to share their calendar with external users. Only for the default policy, so exclusions can be made if needed.", "docsDescription": "Disables external calendar sharing for the entire tenant. This is not a widely used feature, and it's therefore unlikely that this will impact users. Only for the default policy, so exclusions can be made if needed by making a new policy and assigning it to users.", "executiveText": "Prevents employees from sharing their calendars with external parties, protecting sensitive meeting information and internal schedules from unauthorized access. This security measure helps maintain confidentiality of business activities while still allowing internal collaboration.", @@ -2106,7 +2141,11 @@ { "name": "standards.DisableAdditionalStorageProviders", "cat": "Exchange Standards", - "tag": ["CIS M365 5.0 (6.5.3)", "exo_storageproviderrestricted", "ZTNA21817"], + "tag": [ + "CIS M365 5.0 (6.5.3)", + "exo_storageproviderrestricted", + "ZTNA21817" + ], "helpText": "Disables the ability for users to open files in Outlook on the Web, from other providers such as Box, Dropbox, Facebook, Google Drive, OneDrive Personal, etc.", "docsDescription": "Disables additional storage providers in OWA. This is to prevent users from using personal storage providers like Dropbox, Google Drive, etc. Usually this has little user impact.", "executiveText": "Prevents employees from accessing personal cloud storage services like Dropbox or Google Drive through Outlook on the web, reducing data security risks and ensuring company information stays within approved corporate systems. This helps maintain data governance and prevents accidental data leaks.", @@ -2388,7 +2427,11 @@ { "name": "standards.DisableSharedMailbox", "cat": "Exchange Standards", - "tag": ["CIS M365 5.0 (1.2.2)", "CISA (MS.AAD.10.1v1)", "NIST CSF 2.0 (PR.AA-01)"], + "tag": [ + "CIS M365 5.0 (1.2.2)", + "CISA (MS.AAD.10.1v1)", + "NIST CSF 2.0 (PR.AA-01)" + ], "helpText": "Blocks login for all accounts that are marked as a shared mailbox. This is Microsoft best practice to prevent direct logons to shared mailboxes.", "docsDescription": "Shared mailboxes can be directly logged into if the password is reset, this presents a security risk as do all shared login credentials. Microsoft's recommendation is to disable the user account for shared mailboxes. It would be a good idea to review the sign-in reports to establish potential impact.", "executiveText": "Prevents direct login to shared mailbox accounts (like info@company.com), ensuring they can only be accessed through authorized users' accounts. This security measure eliminates the risk of shared passwords and unauthorized access while maintaining proper access control and audit trails.", @@ -4286,7 +4329,12 @@ { "name": "standards.SPDisallowInfectedFiles", "cat": "SharePoint Standards", - "tag": ["CIS M365 5.0 (7.3.1)", "CISA (MS.SPO.3.1v1)", "NIST CSF 2.0 (DE.CM-09)", "ZTNA21817"], + "tag": [ + "CIS M365 5.0 (7.3.1)", + "CISA (MS.SPO.3.1v1)", + "NIST CSF 2.0 (DE.CM-09)", + "ZTNA21817" + ], "helpText": "Ensure Office 365 SharePoint infected files are disallowed for download", "executiveText": "Prevents employees from downloading files that have been identified as containing malware or viruses from SharePoint and OneDrive. This security measure protects against malware distribution through file sharing while maintaining access to clean, safe documents.", "addedComponent": [], @@ -4328,7 +4376,13 @@ { "name": "standards.SPExternalUserExpiration", "cat": "SharePoint Standards", - "tag": ["CIS M365 5.0 (7.2.9)", "CISA (MS.SPO.1.5v1)", "ZTNA21803", "ZTNA21804", "ZTNA21858"], + "tag": [ + "CIS M365 5.0 (7.2.9)", + "CISA (MS.SPO.1.5v1)", + "ZTNA21803", + "ZTNA21804", + "ZTNA21858" + ], "helpText": "Ensure guest access to a site or OneDrive will expire automatically", "executiveText": "Automatically expires external user access to SharePoint sites and OneDrive after a specified period, reducing security risks from forgotten or unnecessary guest accounts. This ensures external access is regularly reviewed and maintained only when actively needed.", "addedComponent": [ @@ -4353,7 +4407,12 @@ { "name": "standards.SPEmailAttestation", "cat": "SharePoint Standards", - "tag": ["CIS M365 5.0 (7.2.10)", "CISA (MS.SPO.1.6v1)", "ZTNA21803", "ZTNA21804"], + "tag": [ + "CIS M365 5.0 (7.2.10)", + "CISA (MS.SPO.1.6v1)", + "ZTNA21803", + "ZTNA21804" + ], "helpText": "Ensure re-authentication with verification code is restricted", "executiveText": "Requires external users to periodically re-verify their identity through email verification codes when accessing SharePoint resources, adding an extra security layer for external collaboration. This helps ensure continued legitimacy of external access over time.", "addedComponent": [ @@ -4620,7 +4679,12 @@ { "name": "standards.unmanagedSync", "cat": "SharePoint Standards", - "tag": ["CIS M365 5.0 (7.2.3)", "CISA (MS.SPO.2.1v1)", "NIST CSF 2.0 (PR.AA-05)", "ZTNA24824"], + "tag": [ + "CIS M365 5.0 (7.2.3)", + "CISA (MS.SPO.2.1v1)", + "NIST CSF 2.0 (PR.AA-05)", + "ZTNA24824" + ], "helpText": "Entra P1 required. Block or limit access to SharePoint and OneDrive content from unmanaged devices (those not hybrid AD joined or compliant in Intune). These controls rely on Microsoft Entra Conditional Access policies and can take up to 24 hours to take effect.", "docsDescription": "Entra P1 required. Block or limit access to SharePoint and OneDrive content from unmanaged devices (those not hybrid AD joined or compliant in Intune). These controls rely on Microsoft Entra Conditional Access policies and can take up to 24 hours to take effect. 0 = Allow Access, 1 = Allow limited, web-only access, 2 = Block access. All information about this can be found in Microsofts documentation [here.](https://learn.microsoft.com/en-us/sharepoint/control-access-from-unmanaged-devices)", "executiveText": "Restricts access to company files from personal or unmanaged devices, ensuring corporate data can only be accessed from properly secured and monitored devices. This critical security control prevents data leaks while allowing controlled access through web browsers when necessary.", @@ -4779,7 +4843,7 @@ } ] }, - { + { "type": "switch", "name": "standards.TeamsGlobalMeetingPolicy.AllowPSTNUsersToBypassLobby", "label": "Allow dial-in users to bypass lobby" @@ -5106,10 +5170,7 @@ "condition": { "field": "standards.TeamsFederationConfiguration.DomainControl.value", "compareType": "isOneOf", - "compareValue": [ - "AllowSpecificExternal", - "BlockSpecificExternal" - ] + "compareValue": ["AllowSpecificExternal", "BlockSpecificExternal"] } } ], @@ -6205,11 +6266,7 @@ { "name": "standards.ColleagueImpersonationAlert", "cat": "Exchange Standards", - "tag": [ - "Exchange", - "Security", - "Transport Rules" - ], + "tag": ["Exchange", "Security", "Transport Rules"], "helpText": "Creates/updates 5x Exchange Online transport rules (A-E, F-J, K-O, P-T, U-Z) that prepend an HTML disclaimer banner to inbound emails where the sender display name matches a mailbox in the organisation. Accepted tenant domains are always exempt automatically. Inactive users are removed and enabled users are added. Any manually configured sender or domain exemptions already present on existing rules are preserved.", "docsDescription": "Creates five Exchange Online transport rules grouped by the first letter of user display names (A-E, F-J, K-O, P-T, U-Z). Each rule fires when an external sender's From header matches a display name in that group, prepends a configurable HTML warning banner, and skips emails from accepted organisational domains. Any manually configured sender or domain exemptions on existing rules are preserved when the standard runs. The disclaimer HTML is fully customisable via the standard settings.", "executiveText": "Protects staff from display-name impersonation attacks by injecting a visible warning banner on emails that appear to come from a colleague but originate externally. Rules are maintained automatically across all letter groups and updated whenever the standard runs.", diff --git a/Modules/CIPPCore/Public/New-CIPPTAP.ps1 b/Modules/CIPPCore/Public/New-CIPPTAP.ps1 index 120a81ff1264..e84584171d84 100644 --- a/Modules/CIPPCore/Public/New-CIPPTAP.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPTAP.ps1 @@ -49,7 +49,7 @@ function New-CIPPTAP { # Create parameter string for logging $paramString = ' with ' + ($logParts -join ', ') - Write-LogMessage -headers $Headers -API $APIName -message "Created Temporary Access Password (TAP) for $UserID$paramString" -Sev 'Info' -tenant $TenantFilter + Write-LogMessage -headers $Headers -API $APIName -message "Created Temporary Access Pass (TAP) for $UserID$paramString" -Sev 'Info' -tenant $TenantFilter # Build result text with parameters $resultText = "The TAP for $UserID is $($GraphRequest.temporaryAccessPass) - This TAP is usable for the next $($GraphRequest.LifetimeInMinutes) minutes" @@ -69,7 +69,7 @@ function New-CIPPTAP { } catch { $ErrorMessage = Get-CippException -Exception $_ - $Result = "Failed to create Temporary Access Password (TAP) for $($UserID): $($ErrorMessage.NormalizedError)" + $Result = "Failed to create Temporary Access Pass (TAP) for $($UserID): $($ErrorMessage.NormalizedError)" Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Error' -tenant $TenantFilter -LogData $ErrorMessage throw $Result } From bdd86024c2b7de08eacc764076ef13b66b87fcca Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 27 May 2026 20:24:44 +0200 Subject: [PATCH 053/202] Add version cleanup --- .../Invoke-ExecSPOVersionCleanup.ps1 | 43 +++++++++++++++++++ .../Invoke-CIPPStandardSPOVersionControl.ps1 | 1 - 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ExecSPOVersionCleanup.ps1 diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ExecSPOVersionCleanup.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ExecSPOVersionCleanup.ps1 new file mode 100644 index 000000000000..700f5df2160e --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ExecSPOVersionCleanup.ps1 @@ -0,0 +1,43 @@ +function Invoke-ExecSPOVersionCleanup { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Sharepoint.Site.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + $TenantFilter = $Request.Body.tenantFilter + $SiteUrl = $Request.Body.SiteUrl + $BatchDeleteMode = [int]($Request.Body.BatchDeleteMode ?? 2) + $DeleteOlderThanDays = [int]($Request.Body.DeleteOlderThanDays ?? -1) + $MajorVersionLimit = [int]($Request.Body.MajorVersionLimit ?? -1) + $MajorWithMinorVersionsLimit = [int]($Request.Body.MajorWithMinorVersionsLimit ?? -1) + + try { + $Params = @{ + TenantFilter = $TenantFilter + SiteUrl = $SiteUrl + BatchDeleteMode = $BatchDeleteMode + DeleteOlderThanDays = $DeleteOlderThanDays + MajorVersionLimit = $MajorVersionLimit + MajorWithMinorVersionsLimit = $MajorWithMinorVersionsLimit + } + $null = Start-CIPPSiteVersionCleanup @Params + $Result = "Successfully started version cleanup job for $SiteUrl" + Write-LogMessage -API $APIName -tenant $TenantFilter -headers $Headers -message $Result -sev Info + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API $APIName -tenant $TenantFilter -headers $Headers -message "Failed to start version cleanup for $SiteUrl : $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + $Result = "Failed to start version cleanup: $($ErrorMessage.NormalizedError)" + $StatusCode = [HttpStatusCode]::InternalServerError + } + + return [HttpResponseContext]@{ + StatusCode = $StatusCode ?? [HttpStatusCode]::OK + Body = @{ Results = $Result } + } +} diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPOVersionControl.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPOVersionControl.ps1 index 816f79cee071..cc43fe2c4b0b 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPOVersionControl.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPOVersionControl.ps1 @@ -123,7 +123,6 @@ function Invoke-CIPPStandardSPOVersionControl { try { Set-CIPPSPOSite -TenantFilter $Tenant -SiteUrl $Site.webUrl -Properties $SiteProperties } catch { - Write-Host "Failed to set version policy for site $($Site.webUrl). Exception" -ForegroundColor Red $SiteError = Get-CippException -Exception $_ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to set version policy for site $($Site.webUrl): $($SiteError.NormalizedError)" -sev Error -LogData $SiteError } From 829ced812ed7c07f63842f63eb6d91f84e326594 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Wed, 27 May 2026 22:43:03 +0200 Subject: [PATCH 054/202] feat(mailboxes): cache mailbox and archive usage metrics Add mailbox usage enrichment to the mailbox DB cache using the Graph mailbox usage report. Also cache archive-enabled status, archive size, and archive item count for archive-enabled mailboxes. Expose ArchiveEnabled in live mailbox results without fetching live archive statistics. Fixes https://github.com/KelvinTegelaar/CIPP/issues/6061 --- .../DBCache/Set-CIPPDBCacheMailboxes.ps1 | 62 +++++++++++++++++++ .../Administration/Invoke-ListMailboxes.ps1 | 2 + 2 files changed, 64 insertions(+) diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheMailboxes.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheMailboxes.ps1 index 4da4e1784514..af466b9be767 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheMailboxes.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheMailboxes.ps1 @@ -26,6 +26,7 @@ function Set-CIPPDBCacheMailboxes { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching mailboxes' -sev Debug # Get mailboxes and user details in a single bulk request + $ZeroArchiveGuid = '00000000-0000-0000-0000-000000000000' $Select = 'id,ExchangeGuid,ArchiveGuid,UserPrincipalName,DisplayName,PrimarySMTPAddress,RecipientType,RecipientTypeDetails,EmailAddresses,WhenSoftDeleted,IsInactiveMailbox,ForwardingSmtpAddress,DeliverToMailboxAndForward,ForwardingAddress,HiddenFromAddressListsEnabled,ExternalDirectoryObjectId,MessageCopyForSendOnBehalfEnabled,MessageCopyForSentAsEnabled,GrantSendOnBehalfTo,PersistedCapabilities,LitigationHoldEnabled,LitigationHoldDate,LitigationHoldDuration,ComplianceTagHoldApplied,RetentionHoldEnabled,InPlaceHolds,RetentionPolicy,RemotePowerShellEnabled,Guid,Identity' $BulkRequests = @( @{ CmdletInput = @{ CmdletName = 'Get-Mailbox'; Parameters = @{} } } @@ -49,6 +50,12 @@ function Set-CIPPDBCacheMailboxes { @{ Name = 'UPN'; Expression = { $_.'UserPrincipalName' } }, @{ Name = 'displayName'; Expression = { $_.'DisplayName' } }, @{ Name = 'primarySmtpAddress'; Expression = { $_.'PrimarySMTPAddress' } }, + @{ Name = 'ArchiveEnabled'; Expression = { $_.ArchiveGuid -and $_.ArchiveGuid.ToString() -ne $ZeroArchiveGuid } }, + @{ Name = 'ArchiveSize'; Expression = { 0 } }, + @{ Name = 'ArchiveItemCount'; Expression = { 0 } }, + @{ Name = 'storageUsedInBytes'; Expression = { 0 } }, + @{ Name = 'prohibitSendReceiveQuotaInBytes'; Expression = { 0 } }, + @{ Name = 'MailboxItemCount'; Expression = { 0 } }, @{ Name = 'recipientType'; Expression = { $_.'RecipientType' } }, @{ Name = 'recipientTypeDetails'; Expression = { $_.'RecipientTypeDetails' } }, @{ Name = 'AdditionalEmailAddresses'; Expression = { ($_.'EmailAddresses' | Where-Object { $_ -clike 'smtp:*' }).Replace('smtp:', '') -join ', ' } }, @@ -73,6 +80,61 @@ function Set-CIPPDBCacheMailboxes { @{ Name = 'Identity'; Expression = { $MatchedUser.Identity } })) } + # $MailboxByUPN is the only lookup that stores mailbox objects. Enrichment steps below + # resolve back through this lookup before updating the objects written by Add-CIPPDbItem. + $MailboxByUPN = @{} + foreach ($Mailbox in @($Mailboxes)) { + if ($Mailbox.UPN) { + $MailboxByUPN[$Mailbox.UPN] = $Mailbox + } + } + + try { + $MailboxUsage = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/reports/getMailboxUsageDetail(period='D7')?`$format=application%2fjson" -tenantid $TenantFilter + foreach ($Usage in @($MailboxUsage)) { + if ($Usage.userPrincipalName -and $MailboxByUPN.ContainsKey($Usage.userPrincipalName)) { + $Mailbox = $MailboxByUPN[$Usage.userPrincipalName] + $Mailbox.storageUsedInBytes = try { [int64]$Usage.storageUsedInBytes } catch { 0 } + $Mailbox.prohibitSendReceiveQuotaInBytes = try { [int64]$Usage.prohibitSendReceiveQuotaInBytes } catch { 0 } + $Mailbox.MailboxItemCount = try { [int64]$Usage.itemCount } catch { 0 } + } + } + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache mailbox usage details: $($_.Exception.Message)" -sev Warning + } + + $ArchiveMailboxes = @($Mailboxes | Where-Object { $_.ArchiveEnabled -eq $true -and $_.UPN }) + if ($ArchiveMailboxes.Count -gt 0) { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Caching archive statistics for $($ArchiveMailboxes.Count) mailboxes" -sev Debug + + $MailboxUPNByArchiveStatsRequestId = @{} + $ArchiveStatsRequests = @(foreach ($Mailbox in $ArchiveMailboxes) { + $OperationGuid = [Guid]::NewGuid().ToString() + $MailboxUPNByArchiveStatsRequestId[$OperationGuid] = $Mailbox.UPN + + @{ + CmdletInput = @{ + CmdletName = 'Get-MailboxStatistics' + Parameters = @{ + Identity = $Mailbox.UPN + Archive = $true + } + } + OperationGuid = $OperationGuid + } + }) + + $ArchiveStatsResults = New-ExoBulkRequest -tenantid $TenantFilter -cmdletArray $ArchiveStatsRequests -useSystemMailbox $true + foreach ($ArchiveStat in @($ArchiveStatsResults)) { + if ($ArchiveStat.OperationGuid -and $MailboxUPNByArchiveStatsRequestId.ContainsKey($ArchiveStat.OperationGuid) -and -not $ArchiveStat.error) { + $ArchiveMailboxUPN = $MailboxUPNByArchiveStatsRequestId[$ArchiveStat.OperationGuid] + $ArchiveMailbox = $MailboxByUPN[$ArchiveMailboxUPN] + $ArchiveMailbox.ArchiveSize = try { Get-ExoOnlineStringBytes -SizeString $ArchiveStat.TotalItemSize } catch { 0 } + $ArchiveMailbox.ArchiveItemCount = try { [int64]$ArchiveStat.ItemCount } catch { 0 } + } + } + } + $Mailboxes | Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Mailboxes' -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($Mailboxes.Count) mailboxes successfully" -sev Debug diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxes.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxes.ps1 index ab877da816d2..c66c5477f9ee 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxes.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxes.ps1 @@ -30,6 +30,7 @@ function Invoke-ListMailboxes { } # Original live EXO logic + $ZeroArchiveGuid = '00000000-0000-0000-0000-000000000000' $Select = 'id,ExchangeGuid,ArchiveGuid,UserPrincipalName,DisplayName,PrimarySMTPAddress,RecipientType,RecipientTypeDetails,EmailAddresses,WhenSoftDeleted,IsInactiveMailbox,ForwardingSmtpAddress,DeliverToMailboxAndForward,ForwardingAddress,HiddenFromAddressListsEnabled,ExternalDirectoryObjectId,IsDirSynced,MessageCopyForSendOnBehalfEnabled,MessageCopyForSentAsEnabled,PersistedCapabilities,LitigationHoldEnabled,LitigationHoldDate,LitigationHoldDuration,ComplianceTagHoldApplied,RetentionHoldEnabled,InPlaceHolds,RetentionPolicy' $ExoRequest = @{ tenantid = $TenantFilter @@ -73,6 +74,7 @@ function Invoke-ListMailboxes { @{ Name = 'UPN'; Expression = { $_.'UserPrincipalName' } }, @{ Name = 'displayName'; Expression = { $_.'DisplayName' } }, @{ Name = 'primarySmtpAddress'; Expression = { $_.'PrimarySMTPAddress' } }, + @{ Name = 'ArchiveEnabled'; Expression = { $_.ArchiveGuid -and $_.ArchiveGuid.ToString() -ne $ZeroArchiveGuid } }, @{ Name = 'recipientType'; Expression = { $_.'RecipientType' } }, @{ Name = 'recipientTypeDetails'; Expression = { $_.'RecipientTypeDetails' } }, @{ Name = 'AdditionalEmailAddresses'; Expression = { ($_.'EmailAddresses' | Where-Object { $_ -clike 'smtp:*' }).Replace('smtp:', '') -join ', ' } }, From 0ebb18803a9580b06c7bfedd4b07103e2ab2da41 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 27 May 2026 23:50:41 +0200 Subject: [PATCH 055/202] implement autopatch --- .../Invoke-CIPPStandardAutopatchGroup.ps1 | 246 ++++++++++++++++++ 1 file changed, 246 insertions(+) create mode 100644 Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAutopatchGroup.ps1 diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAutopatchGroup.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAutopatchGroup.ps1 new file mode 100644 index 000000000000..e5057e045ed3 --- /dev/null +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAutopatchGroup.ps1 @@ -0,0 +1,246 @@ +function Invoke-CIPPStandardAutopatchGroup { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) AutopatchGroup + .SYNOPSIS + (Label) Deploy Windows Autopatch Group + .DESCRIPTION + (Helptext) Deploys a Windows Autopatch group with configurable deployment ring settings for quality updates, feature updates, Edge, and Office. + (DocsDescription) Creates or updates a Windows Autopatch deployment group with Test and Last deployment rings. Configures quality update deferrals, feature update targeting, Edge and Office update channels per ring. Uses the Autopatch API proxy to manage the group configuration. + .NOTES + CAT + Intune Standards + TAG + EXECUTIVETEXT + Configures Windows Autopatch deployment groups to manage update delivery across devices. Autopatch automates Windows quality updates, feature updates, Edge, and Office updates using deployment rings with configurable deferrals and deadlines. + ADDEDCOMPONENT + {"type":"textField","name":"standards.AutopatchGroup.GroupName","label":"Group Name","required":true,"defaultValue":"Autopatch default group"} + {"type":"select","multiple":false,"name":"standards.AutopatchGroup.TargetOSVersion","label":"Target OS Version","required":true,"options":[{"label":"Windows 11, version 24H2","value":"Windows 11, version 24H2"},{"label":"Windows 11, version 25H2","value":"Windows 11, version 25H2"}],"defaultValue":"Windows 11, version 25H2"} + {"type":"switch","name":"standards.AutopatchGroup.EnableDriverUpdate","label":"Enable Driver Updates","defaultValue":true} + {"type":"switch","name":"standards.AutopatchGroup.InstallWin10OnWin11Ineligible","label":"Install latest Windows 10 on Windows 11 ineligible devices","defaultValue":false} + {"type":"number","name":"standards.AutopatchGroup.TestQualityDeferral","label":"Test Ring - Quality Update Deferral (days)","defaultValue":0,"validators":{"min":{"value":0,"message":"Minimum value is 0"},"max":{"value":30,"message":"Maximum value is 30"}}} + {"type":"number","name":"standards.AutopatchGroup.TestQualityDeadline","label":"Test Ring - Quality Update Deadline (days)","defaultValue":1,"validators":{"min":{"value":0,"message":"Minimum value is 0"},"max":{"value":30,"message":"Maximum value is 30"}}} + {"type":"number","name":"standards.AutopatchGroup.TestQualityGracePeriod","label":"Test Ring - Quality Update Grace Period (days)","defaultValue":1,"validators":{"min":{"value":0,"message":"Minimum value is 0"},"max":{"value":7,"message":"Maximum value is 7"}}} + {"type":"number","name":"standards.AutopatchGroup.TestFeatureDeferral","label":"Test Ring - Feature Update Deferral (days)","defaultValue":0,"validators":{"min":{"value":0,"message":"Minimum value is 0"},"max":{"value":365,"message":"Maximum value is 365"}}} + {"type":"number","name":"standards.AutopatchGroup.TestFeatureDeadline","label":"Test Ring - Feature Update Deadline (days)","defaultValue":5,"validators":{"min":{"value":0,"message":"Minimum value is 0"},"max":{"value":30,"message":"Maximum value is 30"}}} + {"type":"select","multiple":false,"name":"standards.AutopatchGroup.TestEdgeChannel","label":"Test Ring - Edge Update Channel","options":[{"label":"Stable","value":"Stable"},{"label":"Beta","value":"Beta"},{"label":"Dev","value":"Dev"}],"defaultValue":"Beta"} + {"type":"select","multiple":false,"name":"standards.AutopatchGroup.TestOfficeChannel","label":"Test Ring - Office Update Channel","options":[{"label":"Current","value":"Current"},{"label":"Monthly Enterprise","value":"MonthlyEnterprise"},{"label":"Semi-Annual Enterprise","value":"SemiAnnual"}],"defaultValue":"MonthlyEnterprise"} + {"type":"number","name":"standards.AutopatchGroup.LastQualityDeferral","label":"Last Ring - Quality Update Deferral (days)","defaultValue":1,"validators":{"min":{"value":0,"message":"Minimum value is 0"},"max":{"value":30,"message":"Maximum value is 30"}}} + {"type":"number","name":"standards.AutopatchGroup.LastQualityDeadline","label":"Last Ring - Quality Update Deadline (days)","defaultValue":2,"validators":{"min":{"value":0,"message":"Minimum value is 0"},"max":{"value":30,"message":"Maximum value is 30"}}} + {"type":"number","name":"standards.AutopatchGroup.LastQualityGracePeriod","label":"Last Ring - Quality Update Grace Period (days)","defaultValue":2,"validators":{"min":{"value":0,"message":"Minimum value is 0"},"max":{"value":7,"message":"Maximum value is 7"}}} + {"type":"number","name":"standards.AutopatchGroup.LastFeatureDeferral","label":"Last Ring - Feature Update Deferral (days)","defaultValue":0,"validators":{"min":{"value":0,"message":"Minimum value is 0"},"max":{"value":365,"message":"Maximum value is 365"}}} + {"type":"number","name":"standards.AutopatchGroup.LastFeatureDeadline","label":"Last Ring - Feature Update Deadline (days)","defaultValue":5,"validators":{"min":{"value":0,"message":"Minimum value is 0"},"max":{"value":30,"message":"Maximum value is 30"}}} + {"type":"select","multiple":false,"name":"standards.AutopatchGroup.LastEdgeChannel","label":"Last Ring - Edge Update Channel","options":[{"label":"Stable","value":"Stable"},{"label":"Beta","value":"Beta"},{"label":"Dev","value":"Dev"}],"defaultValue":"Stable"} + {"type":"select","multiple":false,"name":"standards.AutopatchGroup.LastOfficeChannel","label":"Last Ring - Office Update Channel","options":[{"label":"Current","value":"Current"},{"label":"Monthly Enterprise","value":"MonthlyEnterprise"},{"label":"Semi-Annual Enterprise","value":"SemiAnnual"}],"defaultValue":"MonthlyEnterprise"} + {"type":"number","name":"standards.AutopatchGroup.LastOfficeDeferral","label":"Last Ring - Office Update Deferral (days)","defaultValue":1,"validators":{"min":{"value":0,"message":"Minimum value is 0"},"max":{"value":30,"message":"Maximum value is 30"}}} + {"type":"number","name":"standards.AutopatchGroup.LastOfficeDeadline","label":"Last Ring - Office Update Deadline (days)","defaultValue":2,"validators":{"min":{"value":0,"message":"Minimum value is 0"},"max":{"value":30,"message":"Maximum value is 30"}}} + {"type":"number","name":"standards.AutopatchGroup.TestDnfDeferral","label":"Test Ring - Driver & Firmware Deferral (days)","defaultValue":0,"validators":{"min":{"value":0,"message":"Minimum value is 0"},"max":{"value":30,"message":"Maximum value is 30"}}} + {"type":"number","name":"standards.AutopatchGroup.LastDnfDeferral","label":"Last Ring - Driver & Firmware Deferral (days)","defaultValue":1,"validators":{"min":{"value":0,"message":"Minimum value is 0"},"max":{"value":30,"message":"Maximum value is 30"}}} + IMPACT + Medium Impact + ADDEDDATE + 2025-05-27 + POWERSHELLEQUIVALENT + Autopatch API - POST /api/autoPatch + RECOMMENDEDBY + MULTIPLE + True + DISABLEDFEATURES + {"report":false,"warn":false,"remediate":false} + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + .LINK + https://docs.cipp.app/user-documentation/tenant/standards/alignment/templates/available-standards + #> + + param( + $Tenant, + $Settings + ) + # This autoPatch proxy has been created by Microsoft to facilitate Autopatch group management until native Graph API support is available. It abstracts the underlying Graph API calls and provides a simplified interface for creating and updating Autopatch groups based on the provided settings. + + $AutopatchProxyBase = 'https://intuneautopatchbeta-bwhtaqgefgcyaaa8.westeurope-01.azurewebsites.net/api/autoPatch' + + # Extract settings with defaults + $GroupName = $Settings.GroupName ?? 'Autopatch default group' + $TargetOSVersion = $Settings.TargetOSVersion.value ?? $Settings.TargetOSVersion ?? 'Windows 11, version 25H2' + $EnableDriverUpdate = if ($null -ne $Settings.EnableDriverUpdate) { [bool]$Settings.EnableDriverUpdate } else { $true } + $InstallWin10OnWin11Ineligible = if ($null -ne $Settings.InstallWin10OnWin11Ineligible) { [bool]$Settings.InstallWin10OnWin11Ineligible } else { $false } + + # Test ring settings + $TestQualityDeferral = [int]($Settings.TestQualityDeferral ?? 0) + $TestQualityDeadline = [int]($Settings.TestQualityDeadline ?? 1) + $TestQualityGracePeriod = [int]($Settings.TestQualityGracePeriod ?? 1) + $TestFeatureDeferral = [int]($Settings.TestFeatureDeferral ?? 0) + $TestFeatureDeadline = [int]($Settings.TestFeatureDeadline ?? 5) + $TestEdgeChannel = $Settings.TestEdgeChannel.value ?? $Settings.TestEdgeChannel ?? 'Beta' + $TestOfficeChannel = $Settings.TestOfficeChannel.value ?? $Settings.TestOfficeChannel ?? 'MonthlyEnterprise' + $TestDnfDeferral = [int]($Settings.TestDnfDeferral ?? 0) + + # Last ring settings + $LastQualityDeferral = [int]($Settings.LastQualityDeferral ?? 1) + $LastQualityDeadline = [int]($Settings.LastQualityDeadline ?? 2) + $LastQualityGracePeriod = [int]($Settings.LastQualityGracePeriod ?? 2) + $LastFeatureDeferral = [int]($Settings.LastFeatureDeferral ?? 0) + $LastFeatureDeadline = [int]($Settings.LastFeatureDeadline ?? 5) + $LastEdgeChannel = $Settings.LastEdgeChannel.value ?? $Settings.LastEdgeChannel ?? 'Stable' + $LastOfficeChannel = $Settings.LastOfficeChannel.value ?? $Settings.LastOfficeChannel ?? 'MonthlyEnterprise' + $LastDnfDeferral = [int]($Settings.LastDnfDeferral ?? 1) + $LastOfficeDeferral = [int]($Settings.LastOfficeDeferral ?? 1) + $LastOfficeDeadline = [int]($Settings.LastOfficeDeadline ?? 2) + + # Get current autopatch groups + try { + $CurrentGroups = New-GraphGetRequest -uri $AutopatchProxyBase -tenantid $Tenant -AsApp $true + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Could not retrieve Autopatch groups: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + return + } + + $ExistingGroup = if ($CurrentGroups) { + @($CurrentGroups) | Where-Object { $_.name -eq $GroupName } | Select-Object -First 1 + } + + # Build the autopatch group body + $Body = @{ + name = $GroupName + description = '' + globalUserManagedAadGroups = @() + deploymentGroups = @( + @{ + userManagedAadGroups = @() + name = "$GroupName - Test" + deploymentGroupPolicySettings = @{ + aadGroupName = "$GroupName - Test" + deviceConfigurationSetting = @{ + updateBehavior = 'AutoInstallAndRestart' + notificationSetting = 'DefaultNotifications' + qualityDeploymentSettings = @{ + deferral = $TestQualityDeferral + deadline = $TestQualityDeadline + gracePeriod = $TestQualityGracePeriod + } + featureDeploymentSettings = @{ + deferral = $TestFeatureDeferral + deadline = $TestFeatureDeadline + } + updateFrequencyUI = $null + installDays = $null + installTime = $null + activeHourEndTime = $null + activeHourStartTime = $null + } + featureUpdateAnchorCloudSetting = @{ + targetOSVersion = $TargetOSVersion + installLatestWindows10OnWindows11IneligibleDevice = $InstallWin10OnWin11Ineligible + } + dnfUpdateCloudSetting = @{ + approvalType = 'Automatic' + deploymentDeferralInDays = $TestDnfDeferral + } + edgeDCv2Setting = @{ + targetChannel = $TestEdgeChannel + } + officeDCv2Setting = @{ + targetChannel = $TestOfficeChannel + deferral = 0 + deadline = 1 + hideUpdateNotifications = $false + enableAutomaticUpdate = $true + hideEnableDisableUpdate = $true + enableOfficeMgmt = $false + updatePath = 'http://officecdn.microsoft.com/pr/55336b82-a18d-4dd6-b5f6-9e5095c314a6' + } + } + } + @{ + userManagedAadGroups = @() + name = "$GroupName - Last" + deploymentGroupPolicySettings = @{ + aadGroupName = "$GroupName - Last" + deviceConfigurationSetting = @{ + updateBehavior = 'AutoInstallAndRestart' + notificationSetting = 'DefaultNotifications' + qualityDeploymentSettings = @{ + deferral = $LastQualityDeferral + deadline = $LastQualityDeadline + gracePeriod = $LastQualityGracePeriod + } + featureDeploymentSettings = @{ + deferral = $LastFeatureDeferral + deadline = $LastFeatureDeadline + } + updateFrequencyUI = $null + installDays = $null + installTime = $null + activeHourEndTime = $null + activeHourStartTime = $null + } + featureUpdateAnchorCloudSetting = @{ + targetOSVersion = $TargetOSVersion + installLatestWindows10OnWindows11IneligibleDevice = $InstallWin10OnWin11Ineligible + } + dnfUpdateCloudSetting = @{ + approvalType = 'Automatic' + deploymentDeferralInDays = $LastDnfDeferral + } + edgeDCv2Setting = @{ + targetChannel = $LastEdgeChannel + } + officeDCv2Setting = @{ + targetChannel = $LastOfficeChannel + deferral = $LastOfficeDeferral + deadline = $LastOfficeDeadline + hideUpdateNotifications = $false + enableAutomaticUpdate = $true + hideEnableDisableUpdate = $true + enableOfficeMgmt = $false + updatePath = 'http://officecdn.microsoft.com/pr/55336b82-a18d-4dd6-b5f6-9e5095c314a6' + } + } + } + ) + type = 'User' + enableDriverUpdate = $EnableDriverUpdate + scopeTags = @(0) + enabledContentTypes = 31 + } | ConvertTo-Json -Compress -Depth 10 + + if ($Settings.remediate -eq $true) { + if ($ExistingGroup) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Autopatch group '$GroupName' already exists, updating." -sev Info + try { + $UpdateUri = "$AutopatchProxyBase/$($ExistingGroup.id)" + New-GraphPOSTRequest -uri $UpdateUri -tenantid $Tenant -body $Body -type PUT + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully updated Autopatch group '$GroupName'." -sev Info + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to update Autopatch group '$GroupName': $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + } + } else { + try { + New-GraphPOSTRequest -uri $AutopatchProxyBase -tenantid $Tenant -body $Body -type POST + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully created Autopatch group '$GroupName'." -sev Info + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to create Autopatch group '$GroupName': $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + } + } + } + + if ($Settings.alert -eq $true) { + if ($ExistingGroup) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Autopatch group '$GroupName' is configured." -sev Info + } else { + Write-StandardsAlert -message "Autopatch group '$GroupName' is not configured." -object $GroupName ` + -tenant $Tenant -standardName 'AutopatchGroup' -standardId $Settings.standardId + } + } + + if ($Settings.report -eq $true) { + Add-CIPPBPAField -FieldName 'AutopatchGroup' -FieldValue ([bool]$ExistingGroup) -StoreAs bool -Tenant $Tenant + } +} From e41d5322c698c331c3740b6d9d8d62ceec818971 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 28 May 2026 12:49:03 +0800 Subject: [PATCH 056/202] Update Add-CIPPDbItem.ps1 --- Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 b/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 index 5277b80b8f34..ba74ec1096d8 100644 --- a/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 @@ -101,6 +101,7 @@ function Add-CIPPDbItem { PartitionKey = $TenantFilter RowKey = "$Type-Count" DataCount = [int]$NewCount + Type = $Type } -Force $CountMs = $Stopwatch.ElapsedMilliseconds - $CntStart } From 5b7c5a964b81876324525794b66387574d583c2b Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 28 May 2026 13:56:30 +0800 Subject: [PATCH 057/202] Update Invoke-ListWorkerHealth.ps1 --- .../HTTP Functions/CIPP/Settings/Invoke-ListWorkerHealth.ps1 | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListWorkerHealth.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListWorkerHealth.ps1 index f1753f41d93b..8e28804c62d0 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListWorkerHealth.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListWorkerHealth.ps1 @@ -88,10 +88,6 @@ function Invoke-ListWorkerHealth { $Result = [Craft.Services.WorkerMetricsBridge]::DeleteJob($JobId) $Body = @{ Results = @{ Success = $Result; JobId = $JobId } } } - 'PurgeCompleted' { - $Purged = [Craft.Services.WorkerMetricsBridge]::PurgeCompleted() - $Body = @{ Results = @{ Success = $true; PurgedCount = $Purged } } - } 'ChangePriority' { $JobId = $Request.Query.JobId ?? $Request.Body.JobId $NewPriority = $Request.Query.Priority ?? $Request.Body.Priority From aefa69b052ed5d74d0a965f9918ac358f44a668d Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 28 May 2026 12:31:02 +0200 Subject: [PATCH 058/202] add compliance admin by default --- .../GraphHelper/Get-NormalizedError.ps1 | 1 + .../CIPPCore/Public/Set-CIPPSAMAdminRoles.ps1 | 21 +++++++++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/Modules/CIPPCore/Public/GraphHelper/Get-NormalizedError.ps1 b/Modules/CIPPCore/Public/GraphHelper/Get-NormalizedError.ps1 index 13398c7fddee..0b9c687bc2b4 100644 --- a/Modules/CIPPCore/Public/GraphHelper/Get-NormalizedError.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/Get-NormalizedError.ps1 @@ -73,6 +73,7 @@ function Get-NormalizedError { '*AADSTS9002313*' { 'The credentials used to connect to the Graph API are not available, please retry. If this issue persists you may need to execute the SAM wizard.' } '*One or more platform(s) is/are not configured for the customer. Please configure the platform before trying to purchase a SKU.*' { 'One or more platform(s) is/are not configured for the customer. Please configure the platform before trying to purchase a SKU.' } "One or more added object references already exist for the following modified properties: 'members'." { 'This user is already a member of the selected group.' } + '*is not present in the role definition of the current user*' { 'We do not have permissions to access this resource, try performing a CPV refresh in Application Settings -> Permissions. ' } default { $message } } diff --git a/Modules/CIPPCore/Public/Set-CIPPSAMAdminRoles.ps1 b/Modules/CIPPCore/Public/Set-CIPPSAMAdminRoles.ps1 index e7b98b0f7465..42ce73d32592 100644 --- a/Modules/CIPPCore/Public/Set-CIPPSAMAdminRoles.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPSAMAdminRoles.ps1 @@ -17,21 +17,34 @@ function Set-CIPPSAMAdminRoles { $ActionLogs = [System.Collections.Generic.List[object]]::new() + # Default roles always assigned for all tenants + $DefaultRoles = @( + [PSCustomObject]@{ value = '17315797-102d-40b4-93e0-432062caca18'; label = 'Compliance Administrator' } + ) + $SAMRolesTable = Get-CIPPTable -tablename 'SAMRoles' $Roles = Get-CIPPAzDataTableEntity @SAMRolesTable try { - $SAMRoles = $Roles.Roles | ConvertFrom-Json -ErrorAction Stop + $SAMRoles = @($Roles.Roles | ConvertFrom-Json -ErrorAction Stop) $Tenants = $Roles.Tenants | ConvertFrom-Json -ErrorAction Stop if ($Tenants.value) { $Tenants = $Tenants.value } } catch { - $ActionLogs.Add('CIPP-SAM roles not configured') - return $ActionLogs + $SAMRoles = @() + $Tenants = @() + } + + # Merge default roles with user-configured roles, avoiding duplicates + $ExistingValues = @($SAMRoles | ForEach-Object { $_.value }) + foreach ($DefaultRole in $DefaultRoles) { + if ($DefaultRole.value -notin $ExistingValues) { + $SAMRoles = @($SAMRoles) + @($DefaultRole) + } } - if (($SAMRoles | Measure-Object).count -gt 0 -and $Tenants -contains $TenantFilter -or $Tenants -contains 'AllTenants') { + if (($SAMRoles | Measure-Object).Count -gt 0 -and ($Tenants -contains $TenantFilter -or $Tenants -contains 'AllTenants' -or ($Tenants | Measure-Object).Count -eq 0)) { $InitialRequests = @( [PSCustomObject]@{ id = 'memberOf' From 25fcdc1cf1ba33c650e1030ba0b6aa46e4a743eb Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 28 May 2026 13:16:55 +0200 Subject: [PATCH 059/202] add 404 detection for non-existing roles --- Modules/CIPPCore/Public/Set-CIPPSAMAdminRoles.ps1 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Set-CIPPSAMAdminRoles.ps1 b/Modules/CIPPCore/Public/Set-CIPPSAMAdminRoles.ps1 index 42ce73d32592..5ea7b643fcbc 100644 --- a/Modules/CIPPCore/Public/Set-CIPPSAMAdminRoles.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPSAMAdminRoles.ps1 @@ -96,8 +96,10 @@ function Set-CIPPSAMAdminRoles { $Results | ForEach-Object { if ($_.status -eq 204) { $ActionLogs.Add("Added service principal to directory role $($_.id)") + } elseif ($_.status -eq 404) { + $ActionLogs.Add("Directory role $($_.id) does not exist in tenant, skipping") } else { - $ActionLogs.Add("Failed to add service principal to directoryRole $($_.id)") + $ActionLogs.Add("Failed to add service principal to directoryRole $($_.id): $($_ | ConvertTo-Json -Depth 5)") Write-Verbose ($_ | ConvertTo-Json -Depth 5) $HasFailures = $true } From 25e2b0f2cfa9f73ab3e61d630574b0fe91ce9498 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 28 May 2026 20:50:50 +0800 Subject: [PATCH 060/202] tweaks --- .../CIPPAlerts/Public/Alerts/Get-CIPPAlertAdminPassword.ps1 | 2 +- .../HTTP Functions/CIPP/Settings/Invoke-ListWorkerHealth.ps1 | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertAdminPassword.ps1 b/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertAdminPassword.ps1 index 1cea2229c059..35666a10d4ba 100644 --- a/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertAdminPassword.ps1 +++ b/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertAdminPassword.ps1 @@ -12,7 +12,7 @@ function Get-CIPPAlertAdminPassword { $TenantFilter ) try { - $TenantId = (Get-Tenants | Where-Object -Property defaultDomainName -EQ $TenantFilter).customerId + $TenantId = (Get-Tenants -TenantFilter $TenantFilter).customerId # Get role assignments without expanding principal to avoid rate limiting $RoleAssignments = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/roleManagement/directory/roleAssignments?`$filter=roleDefinitionId eq '62e90394-69f5-4237-9190-012177145e10'" -tenantid $($TenantFilter) | Where-Object { $_.principalOrganizationId -EQ $TenantId } diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListWorkerHealth.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListWorkerHealth.ps1 index 8e28804c62d0..2d5bdcc17ef1 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListWorkerHealth.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListWorkerHealth.ps1 @@ -15,6 +15,7 @@ function Invoke-ListWorkerHealth { switch ($Action) { 'Snapshot' { $Snapshot = [Craft.Services.WorkerMetricsBridge]::GetSnapshot() + try { $Snapshot.Memory | Add-Member -NotePropertyName 'TestDataCacheCount' -NotePropertyValue ([CIPP.TestDataCache]::Count) -ErrorAction SilentlyContinue } catch {} $Body = @{ Results = $Snapshot } } 'Summary' { @@ -100,6 +101,10 @@ function Invoke-ListWorkerHealth { $Result = [Craft.Services.WorkerMetricsBridge]::ChangePriority($JobId, [int]$NewPriority) $Body = @{ Results = @{ Success = $Result; JobId = $JobId; NewPriority = [int]$NewPriority } } } + 'CacheDiag' { + $Diag = [CIPP.TestDataCache]::GetDiagnostics() + $Body = @{ Results = $Diag } + } default { $Body = @{ Results = "Unknown action: $Action" } return [HttpResponseContext]@{ From 99dd88c54197a92ba31c5f03b312c7931bac936b Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 28 May 2026 21:00:14 +0800 Subject: [PATCH 061/202] optimisation --- .../Entrypoints/Activity Triggers/BPA/Push-BPACollectData.ps1 | 2 +- .../CIPPAlerts/Public/Alerts/Get-CIPPAlertDefenderMalware.ps1 | 2 +- .../CIPPAlerts/Public/Alerts/Get-CIPPAlertDefenderStatus.ps1 | 2 +- Modules/CIPPCore/Public/Add-CIPPBPAField.ps1 | 2 +- .../HTTP Functions/Endpoint/Autopilot/Invoke-AddAPDevice.ps1 | 2 +- .../Public/Standards/Invoke-CIPPStandardBranding.ps1 | 2 +- .../Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 | 2 +- Modules/CippExtensions/Public/New-CippExtAlert.ps1 | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/BPA/Push-BPACollectData.ps1 b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/BPA/Push-BPACollectData.ps1 index db57af632bde..a3c9131c0208 100644 --- a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/BPA/Push-BPACollectData.ps1 +++ b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/BPA/Push-BPACollectData.ps1 @@ -5,7 +5,7 @@ function Push-BPACollectData { #> param($Item) - $TenantName = Get-Tenants | Where-Object -Property defaultDomainName -EQ $Item.Tenant + $TenantName = Get-Tenants -TenantFilter $Item.Tenant $BPATemplateTable = Get-CippTable -tablename 'templates' $Filter = "PartitionKey eq 'BPATemplate'" $TemplatesLoc = (Get-CIPPAzDataTableEntity @BPATemplateTable -Filter $Filter).JSON | ConvertFrom-Json diff --git a/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertDefenderMalware.ps1 b/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertDefenderMalware.ps1 index 4b3f6dd962a0..d651440f0cd3 100644 --- a/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertDefenderMalware.ps1 +++ b/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertDefenderMalware.ps1 @@ -12,7 +12,7 @@ function Get-CIPPAlertDefenderMalware { $TenantFilter ) try { - $TenantId = (Get-Tenants | Where-Object -Property defaultDomainName -EQ $TenantFilter).customerId + $TenantId = (Get-Tenants -TenantFilter $TenantFilter).customerId $AlertData = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/tenantRelationships/managedTenants/windowsDeviceMalwareStates?`$top=999&`$filter=tenantId eq '$($TenantId)'" | Where-Object { $_.malwareThreatState -eq 'Active' } | ForEach-Object { [PSCustomObject]@{ DeviceName = $_.managedDeviceName diff --git a/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertDefenderStatus.ps1 b/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertDefenderStatus.ps1 index ec95245fa350..ee8d556ba771 100644 --- a/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertDefenderStatus.ps1 +++ b/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertDefenderStatus.ps1 @@ -11,7 +11,7 @@ function Get-CIPPAlertDefenderStatus { $TenantFilter ) try { - $TenantId = (Get-Tenants | Where-Object -Property defaultDomainName -EQ $TenantFilter).customerId + $TenantId = (Get-Tenants -TenantFilter $TenantFilter).customerId $AlertData = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/tenantRelationships/managedTenants/windowsProtectionStates?`$top=999&`$filter=tenantId eq '$($TenantId)'" | Where-Object { $_.realTimeProtectionEnabled -eq $false -or $_.MalwareprotectionEnabled -eq $false } | ForEach-Object { [PSCustomObject]@{ ManagedDeviceName = $_.managedDeviceName diff --git a/Modules/CIPPCore/Public/Add-CIPPBPAField.ps1 b/Modules/CIPPCore/Public/Add-CIPPBPAField.ps1 index 74c1110550a6..0f119b13f266 100644 --- a/Modules/CIPPCore/Public/Add-CIPPBPAField.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPBPAField.ps1 @@ -7,7 +7,7 @@ function Add-CIPPBPAField { $Tenant ) $Table = Get-CippTable -tablename 'cachebpav2' - $TenantName = Get-Tenants | Where-Object -Property defaultDomainName -EQ $Tenant + $TenantName = Get-Tenants -TenantFilter $Tenant $CurrentContentsObject = (Get-CIPPAzDataTableEntity @Table -Filter "RowKey eq '$BPAName' and PartitionKey eq '$($TenantName.customerId)'") Write-Information "Adding $FieldName to $BPAName for $Tenant. content is $FieldValue" if ($CurrentContentsObject.RowKey) { diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Autopilot/Invoke-AddAPDevice.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Autopilot/Invoke-AddAPDevice.ps1 index fc54f6a3bc74..56dc7c0bda12 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Autopilot/Invoke-AddAPDevice.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Autopilot/Invoke-AddAPDevice.ps1 @@ -13,7 +13,7 @@ function Invoke-AddAPDevice { $APIName = $Request.Params.CIPPEndpoint $Headers = $Request.Headers - $TenantFilter = (Get-Tenants | Where-Object { $_.defaultDomainName -eq $Request.Body.TenantFilter.value }).customerId + $TenantFilter = (Get-Tenants -TenantFilter $Request.Body.TenantFilter.value).customerId $GroupName = if ($Request.Body.Groupname) { $Request.Body.Groupname } else { (New-Guid).GUID } Write-Host $GroupName diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardBranding.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardBranding.ps1 index fc1c34e5be27..050f5d6043f6 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardBranding.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardBranding.ps1 @@ -49,7 +49,7 @@ function Invoke-CIPPStandardBranding { ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'Branding' - $TenantId = Get-Tenants | Where-Object -Property defaultDomainName -EQ $Tenant + $TenantId = Get-Tenants -TenantFilter $Tenant $Localizations = New-GraphGetRequest -Uri "https://graph.microsoft.com/beta/organization/$($TenantId.customerId)/branding/localizations" -tenantID $Tenant -AsApp $true # Get layoutTemplateType value using null-coalescing operator diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 index 97e4306f878b..0c6d45b2c533 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 @@ -44,7 +44,7 @@ function Invoke-CIPPStandardPhishProtection { return $true } #we're done. - $TenantId = Get-Tenants | Where-Object -Property defaultDomainName -EQ $Tenant + $TenantId = Get-Tenants -TenantFilter $Tenant $Table = Get-CIPPTable -TableName Config $CippConfig = (Get-CIPPAzDataTableEntity @Table) diff --git a/Modules/CippExtensions/Public/New-CippExtAlert.ps1 b/Modules/CippExtensions/Public/New-CippExtAlert.ps1 index bae549bf0719..303eaf480376 100644 --- a/Modules/CippExtensions/Public/New-CippExtAlert.ps1 +++ b/Modules/CippExtensions/Public/New-CippExtAlert.ps1 @@ -14,7 +14,7 @@ function New-CippExtAlert { 'HaloPSA' { if ($Configuration.HaloPSA.enabled) { $MappingFile = Get-CIPPAzDataTableEntity @MappingTable -Filter "PartitionKey eq 'HaloMapping'" - $TenantId = (Get-Tenants | Where-Object defaultDomainName -EQ $Alert.TenantId).customerId + $TenantId = (Get-Tenants -TenantFilter $Alert.TenantId).customerId Write-Host "TenantId: $TenantId" $MappedId = ($MappingFile | Where-Object { $_.RowKey -eq $TenantId }).IntegrationId Write-Host "MappedId: $MappedId" From 0cdc2e813bf8e713b8f0402f4735ff93b695e6ca Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 28 May 2026 15:21:15 +0200 Subject: [PATCH 062/202] new auth methods single standard --- .../Public/Set-CIPPAuthenticationPolicy.ps1 | 44 +- ...voke-CIPPStandardAuthenticationMethods.ps1 | 389 ++++++++++++++++++ 2 files changed, 411 insertions(+), 22 deletions(-) create mode 100644 Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAuthenticationMethods.ps1 diff --git a/Modules/CIPPCore/Public/Set-CIPPAuthenticationPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPAuthenticationPolicy.ps1 index b3fedd2310ab..767ce5747c3f 100644 --- a/Modules/CIPPCore/Public/Set-CIPPAuthenticationPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPAuthenticationPolicy.ps1 @@ -5,6 +5,9 @@ function Set-CIPPAuthenticationPolicy { [Parameter(Mandatory = $true)][ValidateSet('FIDO2', 'MicrosoftAuthenticator', 'SMS', 'TemporaryAccessPass', 'HardwareOATH', 'softwareOath', 'Voice', 'Email', 'x509Certificate', 'QRCodePin')]$AuthenticationMethodId, [Parameter(Mandatory = $true)][bool]$Enabled, # true = enabled or false = disabled $MicrosoftAuthenticatorSoftwareOathEnabled, + [ValidateSet('default', 'enabled', 'disabled')]$MicrosoftAuthenticatorDisplayLocation, + [ValidateSet('default', 'enabled', 'disabled')]$MicrosoftAuthenticatorDisplayAppInfo, + [ValidateSet('default', 'enabled', 'disabled')]$MicrosoftAuthenticatorCompanionApp, $TAPMinimumLifetime = 60, #Minutes $TAPMaximumLifetime = 480, #minutes $TAPDefaultLifeTime = 60, #minutes @@ -41,26 +44,30 @@ function Set-CIPPAuthenticationPolicy { # Microsoft Authenticator 'MicrosoftAuthenticator' { - # Remove numberMatchingRequiredState property if it exists - $CurrentInfo.featureSettings.PSObject.Properties.Remove('numberMatchingRequiredState') - if ($State -eq 'enabled') { - $CurrentInfo.featureSettings.displayAppInformationRequiredState.state = $State - $CurrentInfo.featureSettings.displayLocationInformationRequiredState.state = $State # Set MS authenticator OTP state if parameter is passed in - if ($null -ne $MicrosoftAuthenticatorSoftwareOathEnabled ) { + if ($null -ne $MicrosoftAuthenticatorSoftwareOathEnabled) { $CurrentInfo.isSoftwareOathEnabled = $MicrosoftAuthenticatorSoftwareOathEnabled $OptionalLogMessage = "and MS Authenticator software OTP to $MicrosoftAuthenticatorSoftwareOathEnabled" } + # Feature settings + if ($MicrosoftAuthenticatorDisplayAppInfo) { + $CurrentInfo.featureSettings.displayAppInformationRequiredState.state = $MicrosoftAuthenticatorDisplayAppInfo + } + if ($MicrosoftAuthenticatorDisplayLocation) { + $CurrentInfo.featureSettings.displayLocationInformationRequiredState.state = $MicrosoftAuthenticatorDisplayLocation + } + if ($MicrosoftAuthenticatorCompanionApp) { + $CurrentInfo.featureSettings.companionAppAllowedState.state = $MicrosoftAuthenticatorCompanionApp + } + # numberMatchingRequiredState is permanently enabled by Microsoft and can no longer be toggled + $CurrentInfo.featureSettings.PSObject.Properties.Remove('numberMatchingRequiredState') } } # SMS 'SMS' { - if ($State -eq 'enabled') { - Write-LogMessage -headers $Headers -API $APIName -tenant $Tenant -message "Setting $AuthenticationMethodId to enabled is not allowed" -sev Error - throw "Setting $AuthenticationMethodId to enabled is not allowed" - } + # No special configuration needed } # Temporary Access Pass @@ -87,31 +94,24 @@ function Set-CIPPAuthenticationPolicy { # Voice call 'Voice' { - # Disallow enabling voice - if ($State -eq 'enabled') { - Write-LogMessage -headers $Headers -API $APIName -tenant $Tenant -message "Setting $AuthenticationMethodId to enabled is not allowed" -sev Error - throw "Setting $AuthenticationMethodId to enabled is not allowed" - } + # No special configuration needed } # Email OTP 'Email' { - if ($State -eq 'enabled') { - Write-LogMessage -headers $Headers -API $APIName -tenant $Tenant -message "Setting $AuthenticationMethodId to enabled is not allowed" -sev Error - throw "Setting $AuthenticationMethodId to enabled is not allowed" - } + # No special configuration needed } # Certificate-based authentication 'x509Certificate' { - # Nothing special to do here + # No special configuration needed } # QR code 'QRCodePin' { if ($State -eq 'enabled') { - Write-LogMessage -headers $Headers -API $APIName -tenant $Tenant -message "Setting $AuthenticationMethodId to enabled is not allowed" -sev Error - throw "Setting $AuthenticationMethodId to enabled is not allowed" + $CurrentInfo.standardQRCodeLifetimeInDays = $QRCodeLifetimeInDays + $CurrentInfo.pinLength = $QRCodePinLength } } default { diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAuthenticationMethods.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAuthenticationMethods.ps1 new file mode 100644 index 000000000000..a83592d2b056 --- /dev/null +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAuthenticationMethods.ps1 @@ -0,0 +1,389 @@ +function Invoke-CIPPStandardAuthenticationMethods { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) AuthenticationMethods + .SYNOPSIS + (Label) Configure Authentication Methods + .DESCRIPTION + (Helptext) Configures all authentication methods for the tenant including Microsoft Authenticator, FIDO2, SMS, Voice, Email OTP, Temporary Access Pass, Software OATH, Hardware OATH, Certificate-based, and QR Code Pin. Enable or disable each method and optionally target specific groups. + (DocsDescription) Unified standard to configure all authentication method policies in a single place. Each method can be independently enabled or disabled, targeted to all users or specific groups using group name wildcards, and configured with method-specific settings such as TAP lifetime, QR code pin length, and Authenticator software OTP. + .NOTES + CAT + Entra (AAD) Standards + TAG + EXECUTIVETEXT + Provides centralized control over all tenant authentication methods from a single standard. Administrators can enable phishing-resistant methods like FIDO2 and Microsoft Authenticator while disabling less secure options like SMS and Voice. Each method supports group-level targeting using wildcard group names, allowing staged rollouts and granular control. + ADDEDCOMPONENT + {"type":"switch","name":"standards.AuthenticationMethods.MicrosoftAuthenticatorEnabled","label":"Microsoft Authenticator","defaultValue":false} + {"type":"textField","name":"standards.AuthenticationMethods.MicrosoftAuthenticatorGroup","label":"Target Group Name (wildcard supported, blank = All Users)","required":false,"condition":{"field":"standards.AuthenticationMethods.MicrosoftAuthenticatorEnabled","compareType":"is","compareValue":true}} + {"type":"switch","name":"standards.AuthenticationMethods.MicrosoftAuthenticatorSoftwareOath","label":"Enable Software OTP in Authenticator","defaultValue":false,"condition":{"field":"standards.AuthenticationMethods.MicrosoftAuthenticatorEnabled","compareType":"is","compareValue":true}} + {"type":"autoComplete","multiple":false,"creatable":false,"label":"Show Application Name in Push Notifications","name":"standards.AuthenticationMethods.MicrosoftAuthenticatorDisplayAppInfo","options":[{"label":"Microsoft managed","value":"default"},{"label":"Enabled","value":"enabled"},{"label":"Disabled","value":"disabled"}],"condition":{"field":"standards.AuthenticationMethods.MicrosoftAuthenticatorEnabled","compareType":"is","compareValue":true}} + {"type":"autoComplete","multiple":false,"creatable":false,"label":"Show Geographic Location in Push Notifications","name":"standards.AuthenticationMethods.MicrosoftAuthenticatorDisplayLocation","options":[{"label":"Microsoft managed","value":"default"},{"label":"Enabled","value":"enabled"},{"label":"Disabled","value":"disabled"}],"condition":{"field":"standards.AuthenticationMethods.MicrosoftAuthenticatorEnabled","compareType":"is","compareValue":true}} + {"type":"autoComplete","multiple":false,"creatable":false,"label":"Companion App (Authenticator Lite)","name":"standards.AuthenticationMethods.MicrosoftAuthenticatorCompanionApp","options":[{"label":"Microsoft managed","value":"default"},{"label":"Enabled","value":"enabled"},{"label":"Disabled","value":"disabled"}],"condition":{"field":"standards.AuthenticationMethods.MicrosoftAuthenticatorEnabled","compareType":"is","compareValue":true}} + {"type":"switch","name":"standards.AuthenticationMethods.FIDO2Enabled","label":"FIDO2 Security Keys","defaultValue":false} + {"type":"textField","name":"standards.AuthenticationMethods.FIDO2Group","label":"Target Group Name (wildcard supported, blank = All Users)","required":false,"condition":{"field":"standards.AuthenticationMethods.FIDO2Enabled","compareType":"is","compareValue":true}} + {"type":"switch","name":"standards.AuthenticationMethods.TAPEnabled","label":"Temporary Access Pass","defaultValue":false} + {"type":"textField","name":"standards.AuthenticationMethods.TAPGroup","label":"Target Group Name (wildcard supported, blank = All Users)","required":false,"condition":{"field":"standards.AuthenticationMethods.TAPEnabled","compareType":"is","compareValue":true}} + {"type":"autoComplete","multiple":false,"creatable":false,"label":"TAP Usage Mode","name":"standards.AuthenticationMethods.TAPUsableOnce","options":[{"label":"Only Once","value":"true"},{"label":"Multiple Logons","value":"false"}],"condition":{"field":"standards.AuthenticationMethods.TAPEnabled","compareType":"is","compareValue":true}} + {"type":"number","name":"standards.AuthenticationMethods.TAPDefaultLifetime","label":"TAP Default Lifetime (minutes)","defaultValue":60,"condition":{"field":"standards.AuthenticationMethods.TAPEnabled","compareType":"is","compareValue":true}} + {"type":"number","name":"standards.AuthenticationMethods.TAPMinLifetime","label":"TAP Minimum Lifetime (minutes)","defaultValue":60,"condition":{"field":"standards.AuthenticationMethods.TAPEnabled","compareType":"is","compareValue":true}} + {"type":"number","name":"standards.AuthenticationMethods.TAPMaxLifetime","label":"TAP Maximum Lifetime (minutes)","defaultValue":480,"condition":{"field":"standards.AuthenticationMethods.TAPEnabled","compareType":"is","compareValue":true}} + {"type":"number","name":"standards.AuthenticationMethods.TAPDefaultLength","label":"TAP Length (characters)","defaultValue":8,"condition":{"field":"standards.AuthenticationMethods.TAPEnabled","compareType":"is","compareValue":true}} + {"type":"switch","name":"standards.AuthenticationMethods.SoftwareOathEnabled","label":"Third-Party Software OATH Tokens","defaultValue":false} + {"type":"textField","name":"standards.AuthenticationMethods.SoftwareOathGroup","label":"Target Group Name (wildcard supported, blank = All Users)","required":false,"condition":{"field":"standards.AuthenticationMethods.SoftwareOathEnabled","compareType":"is","compareValue":true}} + {"type":"switch","name":"standards.AuthenticationMethods.HardwareOathEnabled","label":"Hardware OATH Tokens","defaultValue":false} + {"type":"textField","name":"standards.AuthenticationMethods.HardwareOathGroup","label":"Target Group Name (wildcard supported, blank = All Users)","required":false,"condition":{"field":"standards.AuthenticationMethods.HardwareOathEnabled","compareType":"is","compareValue":true}} + {"type":"switch","name":"standards.AuthenticationMethods.SMSEnabled","label":"SMS","defaultValue":false} + {"type":"textField","name":"standards.AuthenticationMethods.SMSGroup","label":"Target Group Name (wildcard supported, blank = All Users)","required":false,"condition":{"field":"standards.AuthenticationMethods.SMSEnabled","compareType":"is","compareValue":true}} + {"type":"switch","name":"standards.AuthenticationMethods.VoiceEnabled","label":"Voice Call","defaultValue":false} + {"type":"textField","name":"standards.AuthenticationMethods.VoiceGroup","label":"Target Group Name (wildcard supported, blank = All Users)","required":false,"condition":{"field":"standards.AuthenticationMethods.VoiceEnabled","compareType":"is","compareValue":true}} + {"type":"switch","name":"standards.AuthenticationMethods.EmailEnabled","label":"Email OTP","defaultValue":false} + {"type":"textField","name":"standards.AuthenticationMethods.EmailGroup","label":"Target Group Name (wildcard supported, blank = All Users)","required":false,"condition":{"field":"standards.AuthenticationMethods.EmailEnabled","compareType":"is","compareValue":true}} + {"type":"switch","name":"standards.AuthenticationMethods.x509CertificateEnabled","label":"Certificate-Based Authentication","defaultValue":false} + {"type":"textField","name":"standards.AuthenticationMethods.x509CertificateGroup","label":"Target Group Name (wildcard supported, blank = All Users)","required":false,"condition":{"field":"standards.AuthenticationMethods.x509CertificateEnabled","compareType":"is","compareValue":true}} + {"type":"switch","name":"standards.AuthenticationMethods.QRCodePinEnabled","label":"QR Code Pin","defaultValue":false} + {"type":"textField","name":"standards.AuthenticationMethods.QRCodePinGroup","label":"Target Group Name (wildcard supported, blank = All Users)","required":false,"condition":{"field":"standards.AuthenticationMethods.QRCodePinEnabled","compareType":"is","compareValue":true}} + {"type":"number","name":"standards.AuthenticationMethods.QRCodeLifetimeInDays","label":"QR Code Lifetime (days, 1-395)","defaultValue":365,"condition":{"field":"standards.AuthenticationMethods.QRCodePinEnabled","compareType":"is","compareValue":true}} + {"type":"number","name":"standards.AuthenticationMethods.QRCodePinLength","label":"QR Code PIN Length (8-20)","defaultValue":8,"condition":{"field":"standards.AuthenticationMethods.QRCodePinEnabled","compareType":"is","compareValue":true}} + IMPACT + High Impact + ADDEDDATE + 2026-05-28 + POWERSHELLEQUIVALENT + Update-MgBetaPolicyAuthenticationMethodPolicyAuthenticationMethodConfiguration + RECOMMENDEDBY + "CIPP" + DISABLEDFEATURES + {"report":false,"warn":false,"remediate":false} + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + .LINK + https://docs.cipp.app/user-documentation/tenant/standards/alignment/templates/available-standards + #> + + param($Tenant, $Settings) + + # Map of method IDs used in the Graph API to our setting key names + # 'Id' matches the Graph API response id for lookups from the full policy + # 'RemediationId' matches the Set-CIPPAuthenticationPolicy ValidateSet for PATCH calls + $AuthMethods = @( + @{ Id = 'MicrosoftAuthenticator'; RemediationId = 'MicrosoftAuthenticator'; SettingKey = 'MicrosoftAuthenticator'; Label = 'Microsoft Authenticator' } + @{ Id = 'Fido2'; RemediationId = 'FIDO2'; SettingKey = 'FIDO2'; Label = 'FIDO2 Security Keys' } + @{ Id = 'TemporaryAccessPass'; RemediationId = 'TemporaryAccessPass'; SettingKey = 'TAP'; Label = 'Temporary Access Pass' } + @{ Id = 'softwareOath'; RemediationId = 'softwareOath'; SettingKey = 'SoftwareOath'; Label = 'Software OATH Tokens' } + @{ Id = 'HardwareOath'; RemediationId = 'HardwareOATH'; SettingKey = 'HardwareOath'; Label = 'Hardware OATH Tokens' } + @{ Id = 'Sms'; RemediationId = 'SMS'; SettingKey = 'SMS'; Label = 'SMS' } + @{ Id = 'Voice'; RemediationId = 'Voice'; SettingKey = 'Voice'; Label = 'Voice Call' } + @{ Id = 'Email'; RemediationId = 'Email'; SettingKey = 'Email'; Label = 'Email OTP' } + @{ Id = 'x509Certificate'; RemediationId = 'x509Certificate'; SettingKey = 'x509Certificate'; Label = 'Certificate-Based Authentication' } + @{ Id = 'QRCodePin'; RemediationId = 'QRCodePin'; SettingKey = 'QRCodePin'; Label = 'QR Code Pin' } + ) + + # Determine which methods the user has explicitly configured + $ConfiguredMethods = foreach ($Method in $AuthMethods) { + $EnabledKey = "$($Method.SettingKey)Enabled" + $EnabledValue = $Settings.$EnabledKey + if ($null -eq $EnabledValue) { continue } + $GroupName = $Settings."$($Method.SettingKey)Group" + [PSCustomObject]@{ + Id = $Method.Id + RemediationId = $Method.RemediationId + Key = $Method.SettingKey + Label = $Method.Label + Enabled = [bool]$EnabledValue + GroupName = if ([string]::IsNullOrWhiteSpace($GroupName)) { $null } else { $GroupName } + } + } + + if (-not $ConfiguredMethods -or $ConfiguredMethods.Count -eq 0) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'AuthenticationMethods: No authentication methods configured, skipping.' -sev Info + return + } + + try { + $FullPolicy = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/policies/authenticationMethodsPolicy' -tenantid $Tenant -AsApp $true + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "AuthenticationMethods: Could not retrieve authentication methods policy. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + return + } + + # Index the method configurations by ID for fast lookup + $CurrentConfigs = @{} + foreach ($Config in $FullPolicy.authenticationMethodConfigurations) { + $CurrentConfigs[$Config.id] = $Config + } + + # Resolve group names to IDs (cached to avoid duplicate lookups) + $GroupIdCache = @{} + foreach ($Method in $ConfiguredMethods) { + if ($Method.Enabled -and $Method.GroupName -and -not $GroupIdCache.ContainsKey($Method.GroupName)) { + try { + $EscapedName = $Method.GroupName -replace "'", "''" + $GroupFilter = [System.Uri]::EscapeDataString("startsWith(displayName,'$EscapedName')") + $MatchedGroups = @(New-GraphGetRequest -uri "https://graph.microsoft.com/beta/groups?`$select=id,displayName&`$filter=$GroupFilter" -tenantid $Tenant) + if ($MatchedGroups.Count -gt 0) { + $GroupIdCache[$Method.GroupName] = @($MatchedGroups | ForEach-Object { $_.id }) + if ($MatchedGroups.Count -gt 1) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "AuthenticationMethods: Multiple groups matched '$($Method.GroupName)': $($MatchedGroups.displayName -join ', ')" -sev Info + } + } else { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "AuthenticationMethods: No group found matching '$($Method.GroupName)'" -sev Warning + $GroupIdCache[$Method.GroupName] = $null + } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "AuthenticationMethods: Failed to resolve group '$($Method.GroupName)'. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + $GroupIdCache[$Method.GroupName] = $null + } + } + } + + # --- Build expected vs current state and check compliance per method --- + $ComplianceResults = foreach ($Method in $ConfiguredMethods) { + $CurrentConfig = $CurrentConfigs[$Method.Id] + if (-not $CurrentConfig) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "AuthenticationMethods: Method '$($Method.Label)' not found in policy response." -sev Warning + continue + } + + $DesiredState = if ($Method.Enabled) { 'enabled' } else { 'disabled' } + $Drifts = [System.Collections.Generic.List[string]]::new() + + # -- State check -- + if ($CurrentConfig.state -ne $DesiredState) { + $Drifts.Add("state: '$($CurrentConfig.state)' -> '$DesiredState'") + } + + # -- Group targeting check (compare only targetType + id, ignore API-added properties) -- + $CurrentTargetIds = @($CurrentConfig.includeTargets | ForEach-Object { $_.id }) + if ($Method.Enabled -and $Method.GroupName) { + $ResolvedGroupIds = $GroupIdCache[$Method.GroupName] + if ($ResolvedGroupIds) { + $Diff = Compare-Object -ReferenceObject @($ResolvedGroupIds | Sort-Object) -DifferenceObject @($CurrentTargetIds | Sort-Object) -ErrorAction SilentlyContinue + if ($Diff) { + $Drifts.Add("includeTargets: current [$($CurrentTargetIds -join ', ')] -> expected [$($ResolvedGroupIds -join ', ')]") + } + } + } elseif ($Method.Enabled) { + if ('all_users' -notin $CurrentTargetIds) { + $Drifts.Add("includeTargets: current [$($CurrentTargetIds -join ', ')] -> expected [all_users]") + } + } + + # Build normalized includeTargets for expected config (only the properties we manage) + if ($Method.Enabled -and $Method.GroupName) { + $ResolvedGroupIds = $GroupIdCache[$Method.GroupName] + if ($ResolvedGroupIds) { + $NormalizedTargets = @($ResolvedGroupIds | ForEach-Object { @{ targetType = 'group'; id = $_ } }) + } else { + $NormalizedTargets = @($CurrentConfig.includeTargets | ForEach-Object { @{ targetType = $_.targetType; id = $_.id } }) + } + } elseif ($Method.Enabled) { + $NormalizedTargets = @(@{ targetType = 'group'; id = 'all_users' }) + } else { + # Disabled: mirror current targets (we don't manage them) + $NormalizedTargets = @($CurrentConfig.includeTargets | ForEach-Object { @{ targetType = $_.targetType; id = $_.id } }) + } + + # -- Build expected config with all comparable properties -- + $ExpectedConfig = @{ + state = $DesiredState + includeTargets = $NormalizedTargets + } + + switch ($Method.Id) { + 'MicrosoftAuthenticator' { + if ($Method.Enabled) { + $DesiredSoftwareOath = [bool]$Settings.MicrosoftAuthenticatorSoftwareOath + $ExpectedConfig['isSoftwareOathEnabled'] = $DesiredSoftwareOath + if ($CurrentConfig.isSoftwareOathEnabled -ne $DesiredSoftwareOath) { + $Drifts.Add("isSoftwareOathEnabled: '$($CurrentConfig.isSoftwareOathEnabled)' -> '$DesiredSoftwareOath'") + } + + # Feature settings: each has a .state property + $FeatureMap = @( + @{ Setting = 'MicrosoftAuthenticatorDisplayAppInfo'; Property = 'displayAppInformationRequiredState'; Label = 'Display App Info' } + @{ Setting = 'MicrosoftAuthenticatorDisplayLocation'; Property = 'displayLocationInformationRequiredState'; Label = 'Display Location' } + @{ Setting = 'MicrosoftAuthenticatorCompanionApp'; Property = 'companionAppAllowedState'; Label = 'Companion App' } + ) + foreach ($Feature in $FeatureMap) { + $DesiredFeatureState = $Settings."$($Feature.Setting)".value ?? $Settings."$($Feature.Setting)" + if ($DesiredFeatureState) { + $CurrentFeatureState = $CurrentConfig.featureSettings."$($Feature.Property)".state + $ExpectedConfig["featureSettings.$($Feature.Property)"] = $DesiredFeatureState + if ($CurrentFeatureState -ne $DesiredFeatureState) { + $Drifts.Add("$($Feature.Label): '$CurrentFeatureState' -> '$DesiredFeatureState'") + } + } + } + } + } + 'TemporaryAccessPass' { + if ($Method.Enabled) { + $TAPUsableOnce = $Settings.TAPUsableOnce.value ?? $Settings.TAPUsableOnce ?? 'true' + $TAPUsableOnceBool = [System.Convert]::ToBoolean($TAPUsableOnce) + $TAPDefaultLifetime = [int]($Settings.TAPDefaultLifetime ?? 60) + $TAPMinLifetime = [int]($Settings.TAPMinLifetime ?? 60) + $TAPMaxLifetime = [int]($Settings.TAPMaxLifetime ?? 480) + $TAPDefaultLength = [int]($Settings.TAPDefaultLength ?? 8) + + $ExpectedConfig['isUsableOnce'] = $TAPUsableOnceBool + $ExpectedConfig['defaultLifetimeInMinutes'] = $TAPDefaultLifetime + $ExpectedConfig['minimumLifetimeInMinutes'] = $TAPMinLifetime + $ExpectedConfig['maximumLifetimeInMinutes'] = $TAPMaxLifetime + $ExpectedConfig['defaultLength'] = $TAPDefaultLength + + if ([System.Convert]::ToBoolean($CurrentConfig.isUsableOnce) -ne $TAPUsableOnceBool) { + $Drifts.Add("isUsableOnce: '$($CurrentConfig.isUsableOnce)' -> '$TAPUsableOnceBool'") + } + if ([int]$CurrentConfig.defaultLifetimeInMinutes -ne $TAPDefaultLifetime) { + $Drifts.Add("defaultLifetimeInMinutes: '$($CurrentConfig.defaultLifetimeInMinutes)' -> '$TAPDefaultLifetime'") + } + if ([int]$CurrentConfig.minimumLifetimeInMinutes -ne $TAPMinLifetime) { + $Drifts.Add("minimumLifetimeInMinutes: '$($CurrentConfig.minimumLifetimeInMinutes)' -> '$TAPMinLifetime'") + } + if ([int]$CurrentConfig.maximumLifetimeInMinutes -ne $TAPMaxLifetime) { + $Drifts.Add("maximumLifetimeInMinutes: '$($CurrentConfig.maximumLifetimeInMinutes)' -> '$TAPMaxLifetime'") + } + if ([int]$CurrentConfig.defaultLength -ne $TAPDefaultLength) { + $Drifts.Add("defaultLength: '$($CurrentConfig.defaultLength)' -> '$TAPDefaultLength'") + } + } + } + 'QRCodePin' { + if ($Method.Enabled) { + $DesiredLifetime = [int]($Settings.QRCodeLifetimeInDays ?? 365) + $DesiredPinLength = [int]($Settings.QRCodePinLength ?? 8) + + $ExpectedConfig['standardQRCodeLifetimeInDays'] = $DesiredLifetime + $ExpectedConfig['pinLength'] = $DesiredPinLength + + if ([int]$CurrentConfig.standardQRCodeLifetimeInDays -ne $DesiredLifetime) { + $Drifts.Add("standardQRCodeLifetimeInDays: '$($CurrentConfig.standardQRCodeLifetimeInDays)' -> '$DesiredLifetime'") + } + if ([int]$CurrentConfig.pinLength -ne $DesiredPinLength) { + $Drifts.Add("pinLength: '$($CurrentConfig.pinLength)' -> '$DesiredPinLength'") + } + } + } + } + + [PSCustomObject]@{ + Method = $Method + CurrentConfig = $CurrentConfig + ExpectedConfig = [PSCustomObject]$ExpectedConfig + DesiredState = $DesiredState + Drifts = $Drifts + IsCompliant = $Drifts.Count -eq 0 + } + } + + if ($Settings.remediate -eq $true) { + foreach ($Result in $ComplianceResults) { + if ($Result.IsCompliant) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "AuthenticationMethods: $($Result.Method.Label) is already configured correctly." -sev Info + continue + } + + try { + $Params = @{ + Tenant = $Tenant + APIName = 'Standards' + AuthenticationMethodId = $Result.Method.RemediationId + Enabled = $Result.Method.Enabled + } + + # Add group targeting + if ($Result.Method.Enabled -and $Result.Method.GroupName) { + $ResolvedGroupIds = $GroupIdCache[$Result.Method.GroupName] + if ($ResolvedGroupIds) { + $Params['GroupIds'] = $ResolvedGroupIds + } + } + + # Add method-specific parameters + switch ($Result.Method.Id) { + 'MicrosoftAuthenticator' { + if ($Result.Method.Enabled) { + $Params['MicrosoftAuthenticatorSoftwareOathEnabled'] = [bool]$Settings.MicrosoftAuthenticatorSoftwareOath + $DisplayAppInfo = $Settings.MicrosoftAuthenticatorDisplayAppInfo.value ?? $Settings.MicrosoftAuthenticatorDisplayAppInfo + $DisplayLocation = $Settings.MicrosoftAuthenticatorDisplayLocation.value ?? $Settings.MicrosoftAuthenticatorDisplayLocation + $CompanionApp = $Settings.MicrosoftAuthenticatorCompanionApp.value ?? $Settings.MicrosoftAuthenticatorCompanionApp + if ($DisplayAppInfo) { $Params['MicrosoftAuthenticatorDisplayAppInfo'] = $DisplayAppInfo } + if ($DisplayLocation) { $Params['MicrosoftAuthenticatorDisplayLocation'] = $DisplayLocation } + if ($CompanionApp) { $Params['MicrosoftAuthenticatorCompanionApp'] = $CompanionApp } + } + } + 'TemporaryAccessPass' { + if ($Result.Method.Enabled) { + $TAPUsableOnce = $Settings.TAPUsableOnce.value ?? $Settings.TAPUsableOnce ?? 'true' + $Params['TAPisUsableOnce'] = $TAPUsableOnce + $Params['TAPDefaultLifeTime'] = [int]($Settings.TAPDefaultLifetime ?? 60) + $Params['TAPMinimumLifetime'] = [int]($Settings.TAPMinLifetime ?? 60) + $Params['TAPMaximumLifetime'] = [int]($Settings.TAPMaxLifetime ?? 480) + $Params['TAPDefaultLength'] = [int]($Settings.TAPDefaultLength ?? 8) + } + } + 'QRCodePin' { + if ($Result.Method.Enabled) { + $Params['QRCodeLifetimeInDays'] = [int]($Settings.QRCodeLifetimeInDays ?? 365) + $Params['QRCodePinLength'] = [int]($Settings.QRCodePinLength ?? 8) + } + } + } + + Set-CIPPAuthenticationPolicy @Params + Write-LogMessage -API 'Standards' -tenant $Tenant -message "AuthenticationMethods: Remediated $($Result.Method.Label). Changes: $($Result.Drifts -join '; ')" -sev Info + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "AuthenticationMethods: Failed to configure $($Result.Method.Label). Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + } + } + } + + if ($Settings.alert -eq $true) { + $NonCompliant = @($ComplianceResults | Where-Object { -not $_.IsCompliant }) + if ($NonCompliant.Count -eq 0) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'AuthenticationMethods: All configured authentication methods are compliant.' -sev Info + } else { + $AlertDetails = foreach ($Result in $NonCompliant) { + [PSCustomObject]@{ + Method = $Result.Method.Label + Drifts = $Result.Drifts -join '; ' + } + } + Write-StandardsAlert -message "AuthenticationMethods: $($NonCompliant.Count) method(s) not compliant: $(($NonCompliant.Method.Label) -join ', ')" -object $AlertDetails -tenant $Tenant -standardName 'AuthenticationMethods' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $Tenant -message "AuthenticationMethods: $($NonCompliant.Count) method(s) not compliant." -sev Info + } + } + + if ($Settings.report -eq $true) { + $CurrentValue = @{} + $ExpectedValue = @{} + foreach ($Result in $ComplianceResults) { + $CompareProperties = @($Result.ExpectedConfig.PSObject.Properties.Name) + $CurrentSnapshot = @{} + foreach ($Prop in $CompareProperties) { + if ($Prop -like 'featureSettings.*') { + $SubProp = $Prop -replace '^featureSettings\.', '' + $CurrentSnapshot[$Prop] = $Result.CurrentConfig.featureSettings.$SubProp.state + } elseif ($Prop -eq 'includeTargets') { + # Normalize current targets to only targetType + id for comparison + $CurrentSnapshot[$Prop] = @($Result.CurrentConfig.includeTargets | ForEach-Object { + @{ targetType = $_.targetType; id = $_.id } + }) + } else { + $CurrentSnapshot[$Prop] = $Result.CurrentConfig.$Prop + } + } + $CurrentValue[$Result.Method.Key] = [PSCustomObject]$CurrentSnapshot + $ExpectedValue[$Result.Method.Key] = $Result.ExpectedConfig + } + Set-CIPPStandardsCompareField -FieldName 'standards.AuthenticationMethods' -CurrentValue ([PSCustomObject]$CurrentValue) -ExpectedValue ([PSCustomObject]$ExpectedValue) -TenantFilter $Tenant + $AllCompliant = -not ($ComplianceResults | Where-Object { -not $_.IsCompliant }) + Add-CIPPBPAField -FieldName 'AuthenticationMethods' -FieldValue ([bool]$AllCompliant) -StoreAs bool -Tenant $Tenant + } +} From fc080e4afdecd4076e5a68fe581c75515ce34f26 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 28 May 2026 15:21:19 +0200 Subject: [PATCH 063/202] new auth methods single standard --- ...voke-CIPPStandardAuthenticationMethods.ps1 | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAuthenticationMethods.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAuthenticationMethods.ps1 index a83592d2b056..61e2423c7738 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAuthenticationMethods.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAuthenticationMethods.ps1 @@ -70,15 +70,15 @@ function Invoke-CIPPStandardAuthenticationMethods { # 'RemediationId' matches the Set-CIPPAuthenticationPolicy ValidateSet for PATCH calls $AuthMethods = @( @{ Id = 'MicrosoftAuthenticator'; RemediationId = 'MicrosoftAuthenticator'; SettingKey = 'MicrosoftAuthenticator'; Label = 'Microsoft Authenticator' } - @{ Id = 'Fido2'; RemediationId = 'FIDO2'; SettingKey = 'FIDO2'; Label = 'FIDO2 Security Keys' } - @{ Id = 'TemporaryAccessPass'; RemediationId = 'TemporaryAccessPass'; SettingKey = 'TAP'; Label = 'Temporary Access Pass' } - @{ Id = 'softwareOath'; RemediationId = 'softwareOath'; SettingKey = 'SoftwareOath'; Label = 'Software OATH Tokens' } - @{ Id = 'HardwareOath'; RemediationId = 'HardwareOATH'; SettingKey = 'HardwareOath'; Label = 'Hardware OATH Tokens' } - @{ Id = 'Sms'; RemediationId = 'SMS'; SettingKey = 'SMS'; Label = 'SMS' } - @{ Id = 'Voice'; RemediationId = 'Voice'; SettingKey = 'Voice'; Label = 'Voice Call' } - @{ Id = 'Email'; RemediationId = 'Email'; SettingKey = 'Email'; Label = 'Email OTP' } - @{ Id = 'x509Certificate'; RemediationId = 'x509Certificate'; SettingKey = 'x509Certificate'; Label = 'Certificate-Based Authentication' } - @{ Id = 'QRCodePin'; RemediationId = 'QRCodePin'; SettingKey = 'QRCodePin'; Label = 'QR Code Pin' } + @{ Id = 'Fido2'; RemediationId = 'FIDO2'; SettingKey = 'FIDO2'; Label = 'FIDO2 Security Keys' } + @{ Id = 'TemporaryAccessPass'; RemediationId = 'TemporaryAccessPass'; SettingKey = 'TAP'; Label = 'Temporary Access Pass' } + @{ Id = 'softwareOath'; RemediationId = 'softwareOath'; SettingKey = 'SoftwareOath'; Label = 'Software OATH Tokens' } + @{ Id = 'HardwareOath'; RemediationId = 'HardwareOATH'; SettingKey = 'HardwareOath'; Label = 'Hardware OATH Tokens' } + @{ Id = 'Sms'; RemediationId = 'SMS'; SettingKey = 'SMS'; Label = 'SMS' } + @{ Id = 'Voice'; RemediationId = 'Voice'; SettingKey = 'Voice'; Label = 'Voice Call' } + @{ Id = 'Email'; RemediationId = 'Email'; SettingKey = 'Email'; Label = 'Email OTP' } + @{ Id = 'x509Certificate'; RemediationId = 'x509Certificate'; SettingKey = 'x509Certificate'; Label = 'Certificate-Based Authentication' } + @{ Id = 'QRCodePin'; RemediationId = 'QRCodePin'; SettingKey = 'QRCodePin'; Label = 'QR Code Pin' } ) # Determine which methods the user has explicitly configured @@ -373,8 +373,8 @@ function Invoke-CIPPStandardAuthenticationMethods { } elseif ($Prop -eq 'includeTargets') { # Normalize current targets to only targetType + id for comparison $CurrentSnapshot[$Prop] = @($Result.CurrentConfig.includeTargets | ForEach-Object { - @{ targetType = $_.targetType; id = $_.id } - }) + @{ targetType = $_.targetType; id = $_.id } + }) } else { $CurrentSnapshot[$Prop] = $Result.CurrentConfig.$Prop } From 1f9fb1fd96197138746753ebede0ed11c6df92cc Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 28 May 2026 23:44:50 +0800 Subject: [PATCH 064/202] test invocation optimisations --- .../Public/Invoke-CIPPTestCollection.ps1 | 14 +- .../CIPP/Settings/Invoke-ListWorkerHealth.ps1 | 4 + Shared/CIPPSharp/CIPPTestDataCache.cs | 357 ++++++++++++++++-- Shared/CIPPSharp/bin/CIPPSharp.dll | Bin 33280 -> 41984 bytes 4 files changed, 338 insertions(+), 37 deletions(-) diff --git a/Modules/CIPPCore/Public/Invoke-CIPPTestCollection.ps1 b/Modules/CIPPCore/Public/Invoke-CIPPTestCollection.ps1 index 3c9ca9619568..b386e1e4413b 100644 --- a/Modules/CIPPCore/Public/Invoke-CIPPTestCollection.ps1 +++ b/Modules/CIPPCore/Public/Invoke-CIPPTestCollection.ps1 @@ -169,30 +169,24 @@ function Invoke-CIPPTestCollection { foreach ($TestFunction in $TestFunctions) { $ItemStopwatch = [System.Diagnostics.Stopwatch]::StartNew() try { - Write-Information " [$SuiteName] Running $($TestFunction.Name) for $TenantFilter" - $TestOutput = @(& $TestFunction.Name -Tenant $TenantFilter) + $TestOutput = @(& $TestFunction -Tenant $TenantFilter) foreach ($Entity in $TestOutput) { - if ($Entity -is [hashtable] -and $Entity.PartitionKey -and $Entity.RowKey) { + if ($Entity -is [hashtable] -and $Entity.PartitionKey) { $ResultBatch.Add($Entity) } } if ($ResultBatch.Count -ge 100) { Add-CIPPAzDataTableEntity @Table -Entity @($ResultBatch) -Force - Write-Information " [$SuiteName] Flushed $($ResultBatch.Count) results to table" $ResultBatch.Clear() } $ItemStopwatch.Stop() - $ElapsedSeconds = [math]::Round($ItemStopwatch.Elapsed.TotalSeconds, 3) - $Timings.Add("$($TestFunction.Name) : ${ElapsedSeconds}s") - Write-Information " [$SuiteName] Completed $($TestFunction.Name) - ${ElapsedSeconds}s" + $Timings.Add("$($TestFunction.Name) : $([math]::Round($ItemStopwatch.Elapsed.TotalSeconds, 3))s") $SuccessCount++ } catch { $ItemStopwatch.Stop() - $ElapsedSeconds = [math]::Round($ItemStopwatch.Elapsed.TotalSeconds, 3) $FailedCount++ $Errors.Add("$($TestFunction.Name) : $($_.Exception.Message)") - $Timings.Add("$($TestFunction.Name) : ${ElapsedSeconds}s (FAILED)") - Write-Warning " [$SuiteName] Failed $($TestFunction.Name) after ${ElapsedSeconds}s: $($_.Exception.Message)" + $Timings.Add("$($TestFunction.Name) : $([math]::Round($ItemStopwatch.Elapsed.TotalSeconds, 3))s (FAILED)") } } diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListWorkerHealth.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListWorkerHealth.ps1 index 2d5bdcc17ef1..80177f19e9c6 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListWorkerHealth.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListWorkerHealth.ps1 @@ -105,6 +105,10 @@ function Invoke-ListWorkerHealth { $Diag = [CIPP.TestDataCache]::GetDiagnostics() $Body = @{ Results = $Diag } } + 'MemoryDetail' { + $Breakdown = [Craft.Services.WorkerMetricsBridge]::GetMemoryBreakdown() + $Body = @{ Results = $Breakdown } + } default { $Body = @{ Results = "Unknown action: $Action" } return [HttpResponseContext]@{ diff --git a/Shared/CIPPSharp/CIPPTestDataCache.cs b/Shared/CIPPSharp/CIPPTestDataCache.cs index 5c32b057677d..9f0f639a1d00 100644 --- a/Shared/CIPPSharp/CIPPTestDataCache.cs +++ b/Shared/CIPPSharp/CIPPTestDataCache.cs @@ -1,84 +1,387 @@ using System; +using System.Collections; using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.Json; using System.Threading; namespace CIPP { ///

- /// Process-scoped, thread-safe cache for test data lookups. - /// Backed by a static ConcurrentDictionary so it is shared across - /// all PowerShell runspaces within the Azure Functions worker. - /// Expired entries are swept every 500 access calls to prevent unbounded growth. - /// TTL is 30 minutes as a safety net. + /// Process-scoped, thread-safe LRU cache for test data lookups. + /// Shared across all PowerShell runspaces. Bounded by both a byte-size + /// cap (default 50 MB) and a short TTL (default 1 minute) so that test + /// suites running against a single tenant get fast cache hits without + /// accumulating stale Gen2 roots that cause GC thrashing. /// public static class TestDataCache { + // ── Configuration ── + private static long _maxBytes = 50L * 1024 * 1024; // 50 MB default + private static TimeSpan _ttl = TimeSpan.FromMinutes(1); + + // ── State ── private static readonly ConcurrentDictionary _cache = new(); - private static readonly TimeSpan _ttl = TimeSpan.FromMinutes(30); + private static readonly LinkedList _lruOrder = new(); // head = oldest + private static readonly Dictionary> _lruIndex = new(); // key → node + private static readonly object _lruLock = new(); + private static long _currentBytes; private static int _accessCount; - private const int SweepInterval = 500; + private static long _hits; + private static long _misses; + private static long _evictions; private sealed class CacheEntry { public object? Value { get; } + public long SizeBytes { get; } public DateTime ExpiresUtc { get; } + public DateTime CreatedUtc { get; } - public CacheEntry(object? value, DateTime expiresUtc) + public CacheEntry(object? value, long sizeBytes, DateTime expiresUtc) { Value = value; + SizeBytes = sizeBytes; ExpiresUtc = expiresUtc; + CreatedUtc = DateTime.UtcNow; } public bool IsExpired => DateTime.UtcNow >= ExpiresUtc; } - private static void SweepIfDue() + /// Configure the cache limits. Call before first use or between test runs. + public static void Configure(long maxBytes = 50 * 1024 * 1024, int ttlSeconds = 60) { - var count = Interlocked.Increment(ref _accessCount); - if (count % SweepInterval == 0) - { - foreach (var kvp in _cache) - { - if (kvp.Value.IsExpired) - { - _cache.TryRemove(kvp.Key, out _); - } - } - } + _maxBytes = maxBytes; + _ttl = TimeSpan.FromSeconds(ttlSeconds); } public static bool TryGet(string key, out object? value) { - SweepIfDue(); + Interlocked.Increment(ref _accessCount); if (_cache.TryGetValue(key, out var entry) && !entry.IsExpired) { + // Promote to most-recently-used + lock (_lruLock) + { + if (_lruIndex.TryGetValue(key, out var node)) + { + _lruOrder.Remove(node); + _lruOrder.AddLast(node); + } + } + Interlocked.Increment(ref _hits); value = entry.Value; return true; } // Remove expired entry if present if (entry != null) - { - _cache.TryRemove(key, out _); - } + RemoveEntry(key); + Interlocked.Increment(ref _misses); value = null; return false; } public static void Set(string key, object? value) { - SweepIfDue(); - _cache[key] = new CacheEntry(value, DateTime.UtcNow + _ttl); + Interlocked.Increment(ref _accessCount); + + // Estimate size before taking lock + int itemCount = value is ICollection col ? col.Count : 0; + long sizeBytes = EstimateValueSize(value, itemCount); + + // If a single entry exceeds the cap, don't cache it at all + if (sizeBytes > _maxBytes) + return; + + // Remove existing entry for this key first + if (_cache.ContainsKey(key)) + RemoveEntry(key); + + // Evict LRU entries until we have room + while (Interlocked.Read(ref _currentBytes) + sizeBytes > _maxBytes) + { + string? victim; + lock (_lruLock) + { + if (_lruOrder.First == null) break; + victim = _lruOrder.First.Value; + } + RemoveEntry(victim); + Interlocked.Increment(ref _evictions); + } + + var entry = new CacheEntry(value, sizeBytes, DateTime.UtcNow + _ttl); + if (_cache.TryAdd(key, entry)) + { + Interlocked.Add(ref _currentBytes, sizeBytes); + lock (_lruLock) + { + var node = _lruOrder.AddLast(key); + _lruIndex[key] = node; + } + } + } + + private static void RemoveEntry(string key) + { + if (_cache.TryRemove(key, out var removed)) + { + Interlocked.Add(ref _currentBytes, -removed.SizeBytes); + lock (_lruLock) + { + if (_lruIndex.TryGetValue(key, out var node)) + { + _lruOrder.Remove(node); + _lruIndex.Remove(key); + } + } + } } public static void Clear() { - _cache.Clear(); + lock (_lruLock) + { + _cache.Clear(); + _lruOrder.Clear(); + _lruIndex.Clear(); + } + Interlocked.Exchange(ref _currentBytes, 0); Interlocked.Exchange(ref _accessCount, 0); + Interlocked.Exchange(ref _hits, 0); + Interlocked.Exchange(ref _misses, 0); + Interlocked.Exchange(ref _evictions, 0); } public static int Count => _cache.Count; + public static long CurrentBytes => Interlocked.Read(ref _currentBytes); + public static double CurrentMB => Math.Round(Interlocked.Read(ref _currentBytes) / (1024.0 * 1024.0), 2); + public static long MaxBytes => _maxBytes; + public static int TtlSeconds => (int)_ttl.TotalSeconds; + public static long Hits => Interlocked.Read(ref _hits); + public static long Misses => Interlocked.Read(ref _misses); + public static long Evictions => Interlocked.Read(ref _evictions); + public static double HitRate => (_hits + _misses) > 0 + ? Math.Round(_hits * 100.0 / (_hits + _misses), 1) : 0; + + // ── PSObject reflection (cached, resolved once at first use) ── + private static readonly object s_reflectLock = new(); + private static bool s_psResolved; + private static Type? s_psObjectType; + private static PropertyInfo? s_psPropsProp; // PSObject.Properties + private static PropertyInfo? s_psPropName; // PSPropertyInfo.Name + private static PropertyInfo? s_psPropValue; // PSPropertyInfo.Value + private const int SampleSize = 5; + private const int MaxDepth = 4; + + private static void EnsurePSResolved() + { + if (s_psResolved) return; + lock (s_reflectLock) + { + if (s_psResolved) return; + try + { + foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) + { + var t = asm.GetType("System.Management.Automation.PSObject"); + if (t == null) continue; + s_psObjectType = t; + s_psPropsProp = t.GetProperty("Properties"); + var piType = asm.GetType("System.Management.Automation.PSPropertyInfo"); + if (piType != null) + { + s_psPropName = piType.GetProperty("Name"); + s_psPropValue = piType.GetProperty("Value"); + } + break; + } + } + catch { /* SMA not loaded */ } + s_psResolved = true; + } + } + + /// + /// Estimate the serialized size of a cached value. For large collections, + /// samples a few items and extrapolates. Handles PSObject by unwrapping + /// NoteProperties into dictionaries via reflection. + /// + private static long EstimateValueSize(object? value, int itemCount) + { + if (value == null) return 0; + EnsurePSResolved(); + + try + { + // Large collection: sample a few items, extrapolate + if (itemCount > SampleSize && value is IEnumerable enumerable) + { + var sample = new List(); + foreach (var item in enumerable) + { + sample.Add(Unwrap(item, 0)); + if (sample.Count >= SampleSize) break; + } + if (sample.Count == 0) return 0; + var sampleBytes = JsonSerializer.SerializeToUtf8Bytes(sample).LongLength; + return sampleBytes * itemCount / sample.Count; + } + + // Small collection or single value: unwrap everything + var unwrapped = Unwrap(value, 0); + return JsonSerializer.SerializeToUtf8Bytes(unwrapped).LongLength; + } + catch { return 0; } + } + + /// + /// Recursively unwrap PSObject → Dictionary and IEnumerable → List + /// so System.Text.Json can serialize them. + /// + private static object? Unwrap(object? value, int depth) + { + if (value == null || depth > MaxDepth) return value; + + // PSObject → extract NoteProperties as Dictionary + if (s_psObjectType != null && s_psObjectType.IsInstanceOfType(value)) + return UnwrapPSObject(value, depth); + + // Collection → unwrap each element + if (value is IEnumerable enumerable && value is not string && value is not byte[]) + { + var list = new List(); + foreach (var item in enumerable) + list.Add(Unwrap(item, depth + 1)); + return list; + } + + return value; + } + + private static Dictionary UnwrapPSObject(object psObj, int depth) + { + var dict = new Dictionary(); + if (s_psPropsProp == null || s_psPropName == null || s_psPropValue == null) + return dict; + + if (s_psPropsProp.GetValue(psObj) is not IEnumerable props) return dict; + + foreach (var prop in props) + { + try + { + var name = s_psPropName.GetValue(prop)?.ToString(); + if (name != null) + dict[name] = Unwrap(s_psPropValue.GetValue(prop), depth + 1); + } + catch { /* skip properties that throw on access */ } + } + return dict; + } + + /// + /// Returns a diagnostic snapshot of the cache: entry count, keys grouped + /// by data type, estimated serialized size, and expiry details. + /// Designed for the worker-health dashboard — call from PS via + /// [CIPP.TestDataCache]::GetDiagnostics() or from CRAFT metrics bridge. + /// + public static CacheDiagnostics GetDiagnostics() + { + var now = DateTime.UtcNow; + var entries = _cache.ToArray(); // snapshot + + long totalBytes = 0; + var byType = new Dictionary(); + + foreach (var kvp in entries) + { + var parts = kvp.Key.Split('|', 2); + var dataType = parts.Length > 1 ? parts[1] : "?"; + + int itemCount = 0; + if (kvp.Value.Value is ICollection col) + itemCount = col.Count; + + long entryBytes = EstimateValueSize(kvp.Value.Value, itemCount); + totalBytes += entryBytes; + + if (!byType.TryGetValue(dataType, out var bucket)) + { + bucket = new TypeBucket { Type = dataType }; + byType[dataType] = bucket; + } + bucket.EntryCount++; + bucket.TotalBytes += entryBytes; + bucket.TotalItems += itemCount; + } + + int active = 0, expired = 0; + DateTime? earliestExpiry = null, latestExpiry = null; + foreach (var kvp in entries) + { + if (kvp.Value.IsExpired) { expired++; } else { active++; } + var exp = kvp.Value.ExpiresUtc; + if (earliestExpiry == null || exp < earliestExpiry) earliestExpiry = exp; + if (latestExpiry == null || exp > latestExpiry) latestExpiry = exp; + } + + return new CacheDiagnostics + { + TotalEntries = entries.Length, + ActiveEntries = active, + ExpiredEntries = expired, + EstimatedTotalMB = Math.Round(totalBytes / (1024.0 * 1024.0), 2), + TrackedTotalMB = CurrentMB, + MaxMB = Math.Round(_maxBytes / (1024.0 * 1024.0), 2), + TtlSeconds = (int)_ttl.TotalSeconds, + Hits = Interlocked.Read(ref _hits), + Misses = Interlocked.Read(ref _misses), + HitRate = HitRate, + Evictions = Interlocked.Read(ref _evictions), + EarliestExpiryUtc = earliestExpiry, + LatestExpiryUtc = latestExpiry, + AccessCount = _accessCount, + TypeBreakdown = byType.Values + .OrderByDescending(b => b.TotalBytes) + .ToList(), + }; + } + } + + /// Diagnostic snapshot returned by TestDataCache.GetDiagnostics(). + public class CacheDiagnostics + { + public int TotalEntries { get; set; } + public int ActiveEntries { get; set; } + public int ExpiredEntries { get; set; } + public double EstimatedTotalMB { get; set; } + public double TrackedTotalMB { get; set; } + public double MaxMB { get; set; } + public int TtlSeconds { get; set; } + public long Hits { get; set; } + public long Misses { get; set; } + public double HitRate { get; set; } + public long Evictions { get; set; } + public DateTime? EarliestExpiryUtc { get; set; } + public DateTime? LatestExpiryUtc { get; set; } + public int AccessCount { get; set; } + public List TypeBreakdown { get; set; } = new(); + } + + /// Per-data-type cache usage bucket. + public class TypeBucket + { + public string Type { get; set; } = ""; + public int EntryCount { get; set; } + public long TotalBytes { get; set; } + public int TotalItems { get; set; } + public double TotalMB => Math.Round(TotalBytes / (1024.0 * 1024.0), 2); } } diff --git a/Shared/CIPPSharp/bin/CIPPSharp.dll b/Shared/CIPPSharp/bin/CIPPSharp.dll index c35be9216c3f8a1a482f6ddd2d4344dbcd13979e..79dae0140c352bbf55f95f7c5f9466d8def44454 100644 GIT binary patch literal 41984 zcmeIbd3+q@kvCr5Gt)EE(v0TNY2CJE#@2!43mc5FZ21BkACir^B9Eo9En8B{Gm;Nv z%Mu102xlPNB$$u|maqv4*EV<A}5 zz;aVJnr0deXUED}P_!5VE%@DRD2Y`HpYt1{+o}ROt$13=wC~pR?a%*I&D3@4K6P1E zcB>i!8*Fwaw^hUJX>5a+Ho*qCDqNU^4Fjb&1z}QMu`1q-P~8sryZlB5jqWLh^?C0V zGj7#F#Y1%^s*-sTt!{^ho-8WnW`_{chTGoDH7i zT)Jl;XVQc2eVj=TdiHT9J!tHcOybu^P$!r8)qoA~>v?y9W6Zq;&~^{2G{hd|-CeeK zxeQ~Np?J5UGd7jg4|Ac0KnspD8wmSj^;(~@Fi4bq$j&KPR7WU(`LTGO-G88fXJS?r9J z)=Xwk<38d>*KkW8X-6NaNcR!8?62t~Z0dj1N9szIlk+2b-Htr&OAMU8#JJt(%5-se zAmXaqfx%vWBLhc{aa(SJF7A)IBksB#0TyK72r_O9mg~|UQBTBEw*%e3{6+?jAmgX> z;#sH;_vYDr&5Unmqn8RdGrlnjFun_*<&^Qw;>Iw;-0{u%4bh!Fz6+l`wlJqxNXGHw zn{!*zE5?oQIa>Dk=KOz6+p)s`sO{3@`y2?fXXe<~x%Ty3`+A;zoo8Rqx3Ba0%0APn zjRyf<>A}xB;Z|0O!Ot3b@Uuo9{H&1&KWo&3-hHf55Bm0rMvk#>Ko_C_+5nTF#DW)@ zGxvEwI~H&W#xO%^iMfBvNh(}&sfJWX8bd`8HPF}+W0;|e2)pcvz?K=q3{^zvv?Bsr zY78?}5n-tv5!iBLn4v@jM34)B>%&|VL!h+)C?ZI!@VREjFhdKInH@n?fBPy7=T}YR zmk-^U(_5r+<9iENPxTgCI#adN#1N^|_!uHp8XrTXMiN78-I;1xi%_l;wlcM{(Dvzh zX+2_&S7Odp=(knrn8P*sYx*r$?LX?b9CNq<|C*S?mH%sE4twLTiaG3`|1jp%Rj7^- zH6ljcjwpKYjSQUN#JC;BDs|}-QE$Xsx1*2+890KBpAyB8*F9|_)+K^q&_YHr1OcPS zVF&_7vBMAqj1q?-$WDD^F)Fo>#r4z@MwzchGKROzbL*vt5~Y?Ry{^bw#+T}QM*$|(GxGCKZ~^(fA% zN4cs-`RPmGxP|J<>#y~bt-sh3Kb=7_G_vk>|4H4aZ-f8;%lIE@gVHm%!T-LWbi#s* z_etRh#oEz>_YGpiM5t(AO0Ihq8AC9vkC0F?ScQ(x?YJHbcR%v#DfD>Lx&**v)vJQ-X$8lz=N6Hp8=*X!u z_5I%194uTKq9a(ExRZBfve&nJlTT#yM@Ony;GePtrhAjGe4s#h1d9WA@{gH3K0B5s zVVcQ(%rugIBV7r1NnZUGMzSTBbD@!3m&=)BB!_c3Q;g(Sayf-Y^7pwMy*SS+<$LII zl+Uk`S{SI8Y#RZ?UDsGLWduFe{ho0k6P#MzCFTyC4iITlkKQFsCSC z9!nm&Nfkq`hOb-`}WG!vu{|gSbgtPh-cq^g&6xr z6yn{tTacoCJLS4psf-~|ar|N`)@7G(((_>ROM*(_ds^{(nl$qWtjTV+d$_jU$W}MX zDKQtcjcm%5xi=%jDI+K)Tg82xyjfImO3ckFCbA&(W((e*(ZeaKtVFKN0~r}k8G+tx z6(7%(#VM++Ni4V}SMU=VJ)EM-+L|l#U`B>hMtX9S4`s^Y6jjz_7TlIA_)taL^F z=gNFCBf}{py}8K`XUgIfRThu5k-l8PPh|u-CFWsg_vgy|O{OePiNSg^W`rq&X#q2v$F`BUk1zmO(0HIAsKp zF1r=~mXncyWKM};P}Qe^nJEk#Glhwn7pFtG#4v{Hr~OX4u7xgcO2<+dH@m{PQ5425 zQy80WVZ_+u5gk`vB_$~gL36YQ$>Nw4Mxr@dn@e)<%}V8xvPdp2i{vs>7{BHym&tqd zv-ryEe?h>AISfI-nCdVDxzJwSbE>_hk2bpro~8g|NV3*T0}*Qz-R2&1dUaxC3 zrpTtiLJTo2#0_&zgi0jQb^63oNpv@QlXcKo?@7+Z1)0i(?>T*@36dB@`<<@5@>meY z#1NxQoM*Y|A7L{%VLgWUWj2)VZ$zD9qrj)|a@(jY=B>3cZDbnPHA2=Hk{Zt_7P}hB z*h9t+;A&V;B5ScLb`6Mam*#n=x7cc2*0o?w+25Ql>`dkEZ%B(MdrHKfI>Y)5bn`aZ z0M6xDO8`(O54EFC1?f6923%6NMvuFATeD`@B=rT(>9Rh{7q|5}zKHQWEwJ&{Q(#%w z0o&h{HbJd5(VzG>g5xtsC7i>xYRKDvb|ynP)~2XFh4C-?v2hmtW^rDzE}qF3r&tG( zH|nlm9ClkDX4K<#P1ldFG$NkZ^`NDWzHOIIpDg0w-T-dIP-PZ)OE%^`6uPaCO64fK&)0A+n~Gm!U;r{R&* zP0&*}$|)AO=KdC7s=v4_aCzU!b;cm0NRE$0C46)%gc|Y<>o#1c9CuAQPRK%R7f~ts zYsqyENS?p9Fbgl`pO12B(i^bOF;#yNOqxe9@w;HnY?WYQA7fG{3(Foeh1E-L)GFYp zg|&~a?*E`nDXDl{3@aD(Qyvs)g3C)2Tx6QyQqu$%>=4sNRQ$OM+D3UKDH2N;bCmru z?P7g&I9;T;n>LCm*F0|e8VqmjL7>O>p9=}QDGlo|l&&e6 zW;7T&Z?la3=Vek0*AzQ#rla-FzLb~6-0p5F(TECj?R5>!Lxbjd8(qcqhSxfTvP_o| zi$iI?Z-lGef*~Z8vK~TW!)2<(`n6J}sHZq;ggp^M)rzZOeG+1g`tdfYiBQ(VU`5^N z?gqNMN7X3oR;HtTyJGw2Fv{J*w?T?-zR%^0eG2pl`#knHz*1j<}S%@Zs`ctr`h)G_O@feRBBGQpH7$;sff$xN+L!X5l#7> z+sAqWI+V}J8k-YY1hw#)q~8^#cx-Nq(j@d z^bo;+H^MR_oVWq=g=s4$nkETOL6W6&(d!z0!w#1m}t-l91>Ir)aV_yW;*b(-OUI0~LPx2;C zMm#o07cj$}{A60*2pfelj9#;$DQqO)O$)ZNppo?WoSX~6s5kv38v=gc%{|kvl~`Od z%Eo24r|PqWh<@XfzMsM{bgs&y9k4ZN>E=NBYfMm*hX|xS9!}-`VdrUrqM8v zLccZh4C_m{Tq(O^~%P-b^n?F|+(^pHEF(KR90K8-mFidn0QC6R}34 zcw5wSe7d(VhOSb7^ETJE%anp`mxDh+SBeW`VMztcJ4JF(*n_~O#^4m1x{5;;zw+p| z`^n37;}~`$IHg>l2DfWMi>^-_VXxaxEfPYvj=sShEt zl{YTVt$T60?yiyzncCZx{?QpKJx#aA>ROsOsBA9BItTrn%3x>eC~PrD7KoAm0K9R+ z2)A}&Oo+e;x3nS}bS&jqP`93iptKMoGq_&6b=qtTMT1|Y>g4F$BZubF9OXvXElO2- zx+>2>rSu}ZK2N3VgHFt0S*=10yA24Zujk(4^r;oJ`hHb6Uc>QzcmnDsu~|)d*7ID= z;%#}UuLH?9qTb@DZ_W#FLf9MjeG(HxdDI{A$1wcN>b<4K7S^_A)C`+9_mnhylVNoH zdUx0?xi^|u9L&BKseCg4+ms|%1saF%WM@5dc(mDdA@L7q`>+XGB=i` zZbs|Zh4Xl!XhaK;6fUs-2}$9CI$qHk(NH*4vVm802D7k|GoZt|!KOdL*Zd9a7bEN| z*%0-GePQp-Jr_)U|NZxi1I4&~;(4zKake~SsHocCU?D? z$F`G#v1ddhST@38X2axgBpMBSqJ@z{Ves4>3`aq|h~(I}S;imngd?$+fMV{=%4M7> z*LPGd!_JjcpVg>O3JYPZ)^F~ywlLzidia91>2|()ozR+&;Z`5>W0wM27NZi<$BXUY zw5&L9rG$}=^}I8MKsw&@h7)=_0j)ifln(#aFw^L$`{Zg^`}sOD7bAvuK1Q9-dKtAG z`4||oGQI=IdjP9&w7D3#JMFFe>NI`eU+ESwnaphMH{$1N-!B?X0@5q8L z^(qvM&P4vyYmCie?4KD!`@2&Z4dy7%$_nE-h4-$D`O14&LBLqzFa!Z(slyNi3ZtNleSF9Lt+HBpXN|`cWt_1rM+gW<#iEp`l* zo0fyTF^-d#`EMrWW?yTUjteKGjyvW4kP-A@?$4NesT^SlLd@o!^9e`pq%FsL!DX5} zFBjH_Q}BXm%)J^JM=-;>8)lDS0(0Y>8NJ)x5R)4;4YhLO-!MT=_i@Ny#cBC+EZ^XH z;}@K{1}y7GD5Kc=F(98fyooFBa>ssxYvToaah)kQj&$$7AqeYx2LU6R9z(hHZ(!*} z`Y3jJihc%jBWaomOpFxip*|<8^#7)Qs^MeY;DBFz1DKIuW*1{MZ;f_sbtb(x_ zj8!r=lQFJBY!+kHz>bHE;erkmisW8k>TS^b(B<4D)w>L?Q`_&~AR(X+BjmdqwyLZ3@8Cs^`gxf0{tu&AP(;i~ zUhEwZVQlWgc~PG}%-XEqG7BpY>vupJO~2s32adddVAhw8N6^jz%-qH2W2C6Xk3PH? zndcUuZS>)NK-J+Clrn)cz7j;3Xnr`q`n*!>k5E%rd~Y-m`xCHeu!J9=thfHmL_vKV zYmL7!iZw>^5@aj4-UHdNJRFMV2_yDCCr7X>C zfM_@zj=2~K9?ze0+>ZjVLNOcfR6RCafJIz^QxA56s!cc;4y5p8oN0LgZ1)&UdjXO? zFwOD-n{wKuaK3nOir;bm?ri7hMNH-VJSNZAmr^KSE}=)TlH@|>f#Q?3DHu0FJIp)T7+IFAx(x{X)MetOZ0HbzH5L{*51DaO%UH5UtYp^7XwUtYfljQ57p? zm1ezHnw{78(FtlimvI6fVHEo&fUwG$Q&T@FTw_%*S{p8p)W#4OO8deO#~I0Waq%-Qy-ZWn+zU?kJN=*GgZf63cyua zl~S9%;uw!y>ns3!_ZP#17r^a%3gHRXRM63iy$axWQB_zTrX=1GoUdh*Ghhd5Og^|Gic?4H6rTqf z^ZA<1=K#fgiPkqFjvb3m=J`}ckxU-%6%}*mDwOE~7uKNf<2wINo<+w(0bzb>M%mYz^s|(j4TZ8KtA=i&<9SCR6 zXFeWtA^3+%Ae^md2sG+Lf5bJ4tE1ApU5o{T#tPTMa9l$bi?iCVa>=%T8ZB(UaG{3DmtXmK z@0{7x+%%(kMhjLigwI>_0=|R3F$ojP#(JW+CIN3BO!o9|NwLHVY@B?4@Yc0c2PW)^M~ewW~i7=zb*M1Ou6^g?*o1cNq47~USbqpX0Q z!qUaSDaVqiK}#d+iVV6c%i9H=npNzwlY7xNQ3 zmr|@V|2M)}D)3^_`GDk|U%)ai2X{@li~hZo>+*6D=Uq{{YeGA1DPzw0(SMj|P)g6o zS>j;uvEm?|sAaejxIur`c1`%2o%eN(t^Ex08q^@Ae=kr1&m4oz2Hk=BM(Cj;#_uoe zEHr5hI!*eDhO|hP zcx)ES7?~}jR1tgZBKXsw z{K5@@Ps73>eJ;Qr+fem-Wsv3qqK8N+sVbJ-G>J9*9JLG5cg44hMe=&Fye!C;*9qtA zMXcw}$m`sVDG%|6do6eE5S1fh%}rC4h*NVBWf7Z@)*@+szGDmOtCE(7eUt|3{px== z<|2+xqA^hYwM>;#lcbq7Or3>KGI83+gG^1sXQ`OFRjBE-7G;L$kupwOOq+$eUUYWQ zwQL@ta;kl*9g{njCZlay^gisG7a?GWV z&}Xvp-AsQkX^%0#eOz-N3*0C)Ns_xlBy&WwFEUv5u94=?r zDe%rJ#-|AXkqKu(|4EZexh#)MTpu_asmsf!18%N351ReeX9GT1&hTHV82+Jx;X?x7 z6HdM0YYHFMTIh*j8}bg6t^|Bz;yS=TMK?h{Br-J4F5{ zM~Zs@{~Toa;Yd&3n$cGyKERJheb`0>^BMj+kKs!K8_a&-`2sH%_<7;~TmISLHwc`T z$1-;b{(!jy_z{7X0oHRz-Y{Sx{Gidf!s!&rVU#sPd(yZDseWxC&8Am9AI>YLU34Sh zr5eKr1m7?C3H?rRek7dP^dRtaG=^W*KLhx*i}ADF3=g=O^J~E`@i6Cp4|D!Sa82?? zHRjwQd9RYZdG4=*)9L;$;Ah+?0cUvL#?bBy`2f30--n$KY3}?C#}E8K5vM*G-K}AM zK<_eWdsPJdt&zPs%Q9bz=0StY#qj&C30R}8aZd*PlzS>**wX~K$1?+vOM%B+ za{xaqoIiQklfQ5+%+KyowKRl&#eTamzZm2CVx%t0+k{@%1`pTLn7IL*1R%>?37I1l z&tF}`(ds(i9ln*=mT$1BwZXaAUvE@YX|ilCw%ogg`Z^t~IA*x$d7JumpwHu`KikyC z=t0cg+iD4~>wMnImAZ$LHq}=?*W;n9Y-(-!j(iVYZBtiQ?F4m$O<`>h>Z3Mw*Mw0} z_bLju{K@5^?+B%A$)jr~a(Q1Twk4lx5OtVhTk@%SGE>+2x+kvG12o5`3aaON0<=I; zQojIg5K8GRpikKp>kQE`n_`_Ix{yO4biO=cr5>iWHg&vet|v^F+0+fFUzj%8)Jv#e zn6}x}Pb-E&C2i{FicwHk+0@%r*Mhp*rq);70P03X!Ojtsa7-v=XBqwD6t+dxeFEKq zMXpo#3G_Q`bJ)&zQHOGR&!)`aE&1i-ovP9vubS_vpaPpZTy2zmlff)LF&zJyq0VQ`3v@%&($zZR#(T59e3YLYoR#{T--_6oon%R71T&-9~rT zd?~+%zGGA06>28drG%O1DWUGLsUOdu?iw z_XyISwy6thp9fWegI3nkH}M(wB)Z?G78ZRse=@n7Robts|0RD4Jz!I9Wk1ZHN-qd? zC*2-+Gk+TW!KTjg{w{wy?QY?Gchb{k`GIq3IZjcT`bJp@)Q5$-t#r3gzZ2>@-`#=v zo^$Cvn|dg4Xa2e5#m11^@2#SPbRHGhlveye{&`etQ@@&c%sr24Z0h}qg@JiARZ-&W z^J$(?chb6U=Ad2wW(C~{J<(& zfa3;EyS939U=5ApkVtyLq#x$D6U|d;Ev2DA2i>hG-&?g8fqK}c-Y;JT>a#X=i_s41 zMVqRs;Ivn5>fJKVchaVgS8>{}Z0ZKl@<*GxzLL{C=S%6n`Vi}E6Y4hF>0?`F&R2H+ zFmeefH*a9jm-ZK58CXxBwyEaouD}NR@B&WbKG7ezoUT~Jl=92v^f{rf^PN@R=eeAo zv8g#YRJxp=wJ8_UuAqOisUoCZL9g1>8x#6GSJE+?dK)KFSJF>xDq*ZLK19E=sjG}s z;6wB~o4NpLo%EhftwmZVdE3~s>wFI&Z6g)f)FVjSNToLQZdsovPBk`VqP#dwwW;G( z$K0E!#io8%wL7qh&b6r<%8$9bXrWDgtbBK%i!QRM`p_z)o7!#aoKPyzO;^~|^_9om z3F@|~yDE1F64d9UdG`f2(~wPl!uw%RBR2H~q-~+=Z0egx+d{Y46iw{&Y^A$wDui>a zt@NNxU0i%epobo{sg1=S2lb?)WK?aV=j^n71qW#xybwQVLQEHQ}a;5 zcKWGJorEWP>ECSXZFr)Wes5D>tLXFe(fc;_ZJa0ek#DhTl^JN2eyUZJ*wRlkg}R-d zE_^J|PfKk|i#{C~pmjp2ewL(oR=y;w4KEFeu)my z^+?-IV>a~*{hNWS=%7u#1{mRQ;}nEw@mfO%=O;61bIWY-*ePw}IQJ!KR*Z zQ}Cm-%%IL`GzvQHy z5b6NFlcG3yFa2Dl`9>m-828a{Z0eint@qI%ZOZai1n(nl4cFm1-$tSQib8o4k=A0T zg?#fpAE!k^snS1AtJ0DN&Vw!y>HvL<>Vtebq|x0t?9pjGA0N_l=>tZM>an}liSK^m z*+oD%-G`$f57kuj?*2ZU`Q*y)!KzTFC{lCzt;nm>FOkeagMk@-LlZ}5vN?#y#s0vZ zG{>dg6&&ANxQR6v|^37jjiRp4@g zYXn{@Fb=3u51@hBC|OoS*9(3dU^(3*@MXXnIte%hr6tQ6s8%9vtwh+VI_GTx#QB7r zDCNtE(hf)(c-Zgd>MH>cR&2rdDKTClcp>(TyMQy?Av?xaeQNd1G*ix@X39C#OgV>| zDd$i#+PJ9Z`Rd`g}0nH)Wp?6O_NUiz}(O0ymkn2sYRXDK+R-frnAzUT~6SSG!*YG*GX>LayDc8ir=k!|qo7xxh2-!}@i#UvtmX zo{qfeep37P#D50-m$GB-qx$O;Pk{d|@aJjk3+DoMmxVoT+O4%K^%dGkWUgn0);9?! zFZ$t$^F4lj5@LOa*m;Haju>NcB*W6bK-VON? za1I6!A(gGYLf}zt@`M{Sw(Sd^UeWMPJzDmxXHf9H+KZ7lJbSfu6W{QBQ-7iQUp*&9 zTd#&s)*5@Y0}~D78d$heFVnw0vBW6T?<}e`dbPiJry7Ux$owJ6+hPn#y?UjNgV0Hk zVIS@lI4F>9TL?Z|epLT-V7YNr`$gF*<7NF1h24P9PS|GrT>D1B4&$42WYR9c6O;A> zz5x72)CG4+;I~7Drr%oo2sq?>9FVm=rax8wBqSdxIA+{0lCNt=CcSUGu6-%u_ug;Y zwsF!H@57S%Y4LWe{>AERy`L8UKP=vOSR|j)9*@lTJf*!+&9(k;;oaV+kn3^OeGl-5 zwOJJpdA|$}_pvJ{e#P4>I`;}36!@ILpX={Nj(LBsA1wQ&_c!{d3jYk8d?DWpQu+&0 z?yHcT>pmu&Kk9t0bWHe%X}$NT_BuUUy~sCJ^OarbYth=UifYvweXD`j)}SA1hl{#= zty;a{yuN&kIQ1#ISzky`(V%MypvV0fX)Yd>ag^otzX^D?`%QSV)c+QGV1xf1ZAA0!#mF;~`ZfO{ zSaqBBnATtL4dCA`e9^z$b(Z%Pf4=@`^)LN}y07ege}&E-sL(k|?9ko~RGT|&Z7tq9 zbFxS_h~!L>oGFqV@ve67K#8l&u&cXlojFe|Z_|0iwdp+iK22QT9a=0n-*X!*+3UVd zn^1@uP224Iuz8y{rSu@JK$Ljgh@&p|o2{0$5Mcf+m_l69LboM!-dMF5pGN zZwG7@ZHuUj`U@6|&W*S~)L#%6ewXk^g)=6c1H!omlCwh(3jdJsKPmhp!Z|9OXR*ti z9eP%Do)pfHh4W+KXd2hTrLl}lOWUvz_`FMfrkVh6?jshX0p87 zWJ_uVZxz@koVeg)0uKp1D)6L0ciw%pCRCfp8d~$X_2c0DuwYE!A%RB)o|IHApHpk{ zxewIlv-|?VTfzTDL0mXv0uKr2NIq*mDx4F7pA?*MvW9jEaIPA`YXhQ9@K(V$3LY1k zQNhQA|DfQ91V1A9QNf=Jd>%Sa2jit-{|Z_^7}K1s)N2 zLf}b}p#s)ZBe1qW>=e9J@Qs4U3+^LVXjJeq;Xf$&A;FIbenQ}z;LHwDNOTIU4RH^z z4RH@&Ae`0^m$FgtxZvG@hycPlB%C9HpAdKwd4E{&rpQoOY8{qZ3%)>LYnWv=3LY2O z4Vl@YQNhOq9ujy|@}7jeE2KqOTdlxWfo%~Pzk-he-=2R+U`_M@_XmL+1&#`QP~Z`P zCj?RfeQqV7I9=aBJe~JTS&#?Pk{>rZWK7G_$8dW zK;TA!qXKJ6S+jLvGjW!2+ zyz@}f-M$u_t*j5u2CT1Se6*bL?(*{h@2ldImzwIuj8C`_aL+`RiB_{rYdPb;tYZ8g zk$*+xe^J5mAFW{dcdAwaURKcoI4gK5;8&`y1bnkL4mi7*@u^h_z`qGF?kQ%zYmx=H zcw&n2id}%W8^eGT1rJVO{x7STe@{8X`zjfp7h;)tuy7)4=q|n)@a+P&ZGmv!kzC)c;M9YnGu!f+V*gho zcO&&hkxY5-2i`5P(064}wGyB8c<}b(k3U`O2lS(rbhK6gFc06EprggYfMK+wj+QF| zEWw|SHZ21zL%ZtuTwWz$Ilf;<$1c4Vu#zSLR^cm>n;F2{0ChYoGYj|`Akla>N*#*=FrzENN~@U3(a@E$-N_XSo0-wvqb`MXuX z`v7%3W4Q*n1&A*Q!#7H3IEiQnp2TMsHLQ%*0Uv}d8Vv#Jc=F~_;JW~I+6`OqoB*Is zSHTtytE4M|?}aTI##$%v5!{>6Xg?rMPqE(8aKh3B{Ax-7KLDuXiJ&dOuLIQSAne7H z27o$FVYUOm0Z_*?K7GJ%1k~~5js^G`SgO%y0d=fxQ@}q5h!a6rs$mB*1pF{8)#(}7 zsMD8VolakdT{?Xg7KLcNHWhG#R*!yg8SpS&0emj%Y~h^aBXl>tfZg~pdXwIz_wekK zTk~q=+A8f5EvXG@!`ctD|D)-8r9MTUtuN6p(Rb;O>*u;|aXsq#d)Iedru$>==iL)M z&7Osxi#$6#U-Ep*^Ut1;aj$XG_=7R%-Rr%@`*+?~z2&}1zG=R&f3km$zsG--|HuB{ z`Tb_IIomwn>@kPU+sy}`A@l?HD4%#`zdnHzJ2Q*%Me}T&qMgc{jg#wBd5_7tUvAzK zIrYrt-H#K-Q}ZssiR-Dn=f>$fV*UUp#r*vwUijDx-+D35{1|V3L`f4P&O}5Gun3~+ zclcvGxc)@+7<4zDqUZDQW_~Of*QyeiTt8jnma9?fk?S8s9+4Q&%JZ4|J0-aAIG6k; zy(0NdS|s`L?NgE;PZ^eQev`JA@XTP+$r2nV;@U3xD+u3BhF7(AmFTV3_K6>AZMqip zWAtV1dFs*si5}EnqT~9@^b7qp;NPQW*AHl`>kYcj^-uJG>m_>5^((r_U8CLSuEP6Q z^ij`#?aRh~?Rn#6`l9y{?IrJ%+E2Zo*M95$f|lp|BHk}*m;1h_z3=-ZnrYtB#9&)bd`rKT8tmyx&EJ$xXixNax2E>=cg^nx z50ZAKGv%=*T4r_vYwg-G)RRoCu)4OhO2uRa(`R#XM>5{EeYIuvwkLWMU4uPVKTD}J zkvb)7iPazqhA9(V9%g_srVZLMwXu zw){>?pGhQRKKr?$85 ziuVlK;!H!w+C;p2Re$fE_JMdmNa+Ebv@$Wc)#_e2)YA*4T-GA1Zy=sj_~IQy@!p=n zJz2c8dRszSHfV9)OdVL!V4}}<3OjfDnY=rlmiG_NK(gHoXA-m};9y|sX2@|c68f#R ziEi}JuEF`U(DRhX`LemQv$fyq-_vIer8@QuB$_v#jyoOPB}4sPxLlOjvp(KClvo|_ zNrHrfyM~fU^s2TV>Dlq*9*`b5}Axa1L!r40f*TSTZNA1lai4 zhVj7?ry4k8qvT8HoqLA7;HTiFq0}No4mJUdl~(sqZ(=^pTb=CL86Qk6??dt)NaKsRsm4f*))G{ot(PGRgeNdVsH;&1Yi`0;@wXclt z2F6MiHfvUMYuof$a~3XcZk{>&oEeK|pR;gb^Q9>-5ZAkuJxG)(VqJeKzB#d~pVN91sF$55L$lGBBzw?- z`n%D4IOD(o9Js_vq6aXoT(-I$k=W|p2}Y`OAeAP#dOU7>Fppe?#Cci7{GQIvB|V8= zaCyzJE*0OB$l~$HSA9B_#a-Nw4+JH$IZM!$5m>WX?L%E%iBu|wx3cFHMk~VjPFc!i zXSLn6HqmG8jAMvoY+T+CMo+g4plp76t&`2fsL5KHWT&(z5CAh;m-Xb3yt3jRmo2hZ zMd55#hc&PaE4yqK(^-0Rhwc3Bot+CYwDD0Y+vBGW*YOfEM#r-krANT{H03-V4yQ>w zwd;=;Ii=5!7g%Bq^>>eFXL`!`G@dZVbK&*17+jh;UhkrK*VaUL>)>fqd9IO|Dm_=G0EMmPo=nRI z2M5#wocd%PPRmog?q8L>WGiAW*6LjeS`_c^N?4(LJ(0b?q2+Bo=n^U3yzrX)bekGli*~|PFOh6nyuTZwJn5`X zS_2oy`x1mg6CDr`Kd}9YZm8;4JJJRtkeTG&?Ar z$X>kIf>f%@8c;33V_nJ=gauY&GZGg{Ez((Fd1`q-G@(&fZB{v8`>IXbV4jd@SCt=A zh>by$-LEC3OZ9QV+p+b^@{}|gHi$j_@!sXw{bR!sPbIKc=uPldIagV0$3QXI!Lfu` zm0UM~Ne((C&{JDt)6kYJiR8khwM&{sT!QshOJ}DzL{=6(y@{kOu#gjj0dw@GA?Q$< zxiK?5XJeb;j4L9O)QU;FZ&UA{j-J769pl<6lcOV^gy)ywSN9UTtmO7gf>X+}o^C8< zGJL0qC8)!C1R)Hw@rld(H_M6#iSgbuN;w@?#%e~%+QjB|tg{C9ti_rKe=>INp{1vO6mw>%1W&y&NkwbuN(MIjUvB z*uN)}V2>2x5B6;8;gf+(DtCA3jB&ArarN~`6WG|F;8%4?2>)k>nFHXH%(;Ub5)E`0k#*p+4sfipzpG5LK}Q z0-pBW5=`=#C(#oW8{0C6;K|xv@n@J$AI1-5Ht{+$zasTg5bsXC!B*m}WXF-!vn=PfT+% z+r%`>UKXe6>`h{tdFmc9oshdlOtZ6gh-u!b8^m;i><`nFEOOHnk8!m-OtaFP!!#$o zH%xP6Yv@o;6l?A8UW7$Kk``gm({i<;M}zL{!AkrL=fjxw2Rl1Asl#IGMAVU3N*xP_ zHg86*P3oeSwX#5jN6`P0_DWY+YQ>hs+F)}W$NzF-F<@b?OLVJF=m@m)F~(quwWV2E zy%buV`8Mm+8(c(lIo$w?P5{V}0O1$X_8Ks?I+4WWu@h-Ln$q(~QbmB=wn;BfvC>jA zg&f8+)zqb+Tvl2LbYQlooP(&9DW^YY-HJgd!@@V40eiDaEU|ML9Tx8`ctLA#746ZF zW4jzidMl|$YKBA@cLbLttv(0GT!MhOwS9YHm&3{llWJ#@u7fipN`qMg?7tqIvN%TDYQ={$9;v21j1?|BG2Hk-FF-OkYwjLd zq;j^!B9zNvd$RY&%3FNm=_u|@*d?&26E>)m=fpH6wQ`P`R9ZTmh#4x9h;ke4?2{uX z;x<^>(~tEroaZcI(8z4xO6j8o%2Oj$eaM#zeS)VV#ehvQIMKE1GN;+02=WI16so@y}XFby~6PGP`| zbYi+)c(tKgS1PMWUe98(S9){eEMXmlWutR4r@ZIfFG<-=lSOaEL0KjuJKZ-kiQ65k zpLZ=Q5ukdo0Oc4oJu7~wW{vMouP?G@e7ow-p@)d3k3ispbV6?EStV-$cvV7N`v6wg zX_lH4o%yoKJ}r@&X5-H0+~M&6IhDsXIUUc*!|Cj641sKxJX)Ix41WM(ayrAV3@rvi(2W3J+DJEjp@yc>XPV9$~q8D*HRN;(|) zRNZVxbE2C%$1{v(&RNvb%9el*8-tZL)?ux%b|sQLqOh6jA0)XI%t5>tWS4!vk~(?$ zoWl{h*6zNZ{?)zl{333lvj|CDyxVfp5@}Sr zsoV)s24iVBh}%1h&}{)#LMQHHk7J6;w*HhO)15|D``dgardB9v(HxTRJ;8^Ac4~*c zrGcFk?izAlN!VQVDU5kp zr<|($W==h+GoD4TFwb}D)Kg@$$PUDR*}D?jI)8bk#I4-tsK*qEH?Qb)Nbk# zDE4Qc%&IEnaMWy*QYW{{$-MTm4{IfZEXEv0$JRvu!adIU09w+yZzZ>%;R1EtWOeTm zZFodL^*%;9hlf#rtW>rmxfgHTPlZAY1*FpAj>2|4M#mqhz;P%37&=9= zLN0O%3hIHLAv`jeoj%=8?*Kn8x(8535@jt0#0UQI&S;yFmbUO1;xeRSah;Vu(@t*( zH;(rZ(iTCgJEzQ9kg_Y;s_N)ya;$dhwHEnyKnq)(T~3yp#-d8aVi-RNV|#Lnauh8C z|5OF+mX+Y2>N3YA?D*_FPUX3cccOaSzTHyIQ=7$c!(u#pIRGhkIc^XG;&8^!n{g_; zgKb%Xv@SeFc?oppT07g8Xtg`RPH1wZGkVU+xOGMoBrTLSAXak2a_49Qrw14}YYOkp zsGlQAMch!Q=Agw+`l*@*M9UzwW;$jQGz>u}&b08iJ)?U$jv&(1e@oL@TTpHs_1gNu zwaHtWmZPan7-N4`t{H6k7WkFO`QWk6S|Jp_czlejXe&DLcn5(cKMw zyTo5y&LDpy@F`y9mfI{XnZWbNv+*3V(@Ny!K4;+zSwARGTDt6vEp~Pje9ZmiuWaE9 zxJ`Os#{gP~!w1`*lwQZ-;bEvixSu>s_e#< z`{NeWkK2yJYuYFHYMYDkY&F$&Nk1NfhRu?fM{y%4l~=XChOfR!AU%#2>gBdNE1oX# z1M54fPpnbVpIaf*qG?O&xI`x$v%RO4YIQu%fxioOa10#8v(!A|9Up$6Epxd~E9sQ@ zYKyd$)3ZJxW=u_?yiM>WM_vy2?8nnu>Dr5rPVh9qzTRTXcH?U_*!lsYi`ro^>){yL zgt*LYrM%Mz9@nD@&(d>r;V8vrsi@n8mTku~@|*DNyoyCsv>Mj%1jW6M<#_s_Oa6N$ zw)nU3*3dTb<#*ig^G7wSvH>yTRHyrxssy(tRtme)V=!m*HeXT1r$;HOxr`{`t(jiUVR?%pW0e{| zqE0sEt=Z7{7bU|h8USEGP>*VU{J2wOtS(qt5h}trF0%Tl9(6_W^LZ#j$J>Rs z8*fj9i^~Rc3MH%+h8}W|Qphpco>eca?sG`UkJy;oJhla-*@tu<4(UYc7qbJ$Azba;;z;XM0r z@zoI1Xh_9|Em2}a+q^~L(H9}XJ%BN=VQd>`K+Y%yP|CGBlCDS5!3iN_6>noPJcGPb-nq(g&Us#CZs z73`H-l@|qht)d~T zj&QeQGSHlaU=C$Ewe*=mb1DxC=~(7?Sg8t82?aHVw6`@rY6lXULgV__|5dfky_W_a}V$TBy+ z;6CWV4+e%zj3jj7$TFWqnNU=daq>EX3YuE?`FtMq$MpuAApn z73_Fe;(>PYO$7v4JEjcC^`#Yzrxp` z=z-^Xf;g(NhOYo-1}o6#g$>2y@$l$3VY_W(n;)SlGBy?&o2xoTkiE{fsrFJM+hYZ)_7B*D=rQr|7r5Ro-)ArA+#i0; zJlFTaaP<8b8{b&th6~8;1~8W5Xbj)t3KT1RcQ^C#15NL{cQf{{_ynZ8c}RYZqJ}>W zN%-bnd<&f6(+%CHS|4p^L@veqN{$?uT1<7tjAWQBqh=c$xzgdRW+{%l_7n;Np;R|2 zpef--g!`3l1pIKC3MZUR#;MB)Z-B@=B-tx78^bgq{U)nWLCK~+$>3#{e2uZgoT|nn*01}IyD zIwm574uddqFzsU63x)}Chhq$fUBnCYFm!iW!RBz$8-jRis#tvGj)$dLmmaAJ`b1@r zR}>hrTnf4ZI|4xF*+5l zVgyHUdje-P$ZKQ?)*N1aCP)>`eTX1f_%TF(LG)0=ic!->3wZo;49_H~xhE@wpi008 zWmv5sH1Q&UV^D<&1WYs1?2hpoA3$TEGjK6*Gw?7l7nvZjj{cPzp)cX*Df!G0p~{GBb$P52ecj$mQ>VOM^Q3rpC$EkmKqkpFRJyw%|8-@JBR5qb4EhTV}V+=$bRPWlneV^i7GDmN}aeT{Gs) znzLz6qB}8r_RLLP-Ltw{X5xFY-8=-QYt+d4n{w}n)t~tTkA&aOnta+U>Ca}_A5JC= zGI#;LP%juSP5}+$(f#-Z0>3RRr=xAQyykNgBZC@}1WWjMq3+?sadL91(8kCEI#?SK=NL_2HTFWFjF? zVsf7M>!2ev5#J$EldIzYQU7ZmP`~erFIpYVEke2q?yQaHv)nSgHypw5*r@KvEZ{Q% z*Hb$_sJI?;`C4Ghae{a;=;eS*fb;7s?q9zry_q|sywnvZd-CG$u0n#%z@-gu4O;m; zZwb!R__UMn7s*K>((r2mfSH?Zd?xLjWFi5t&$B59J*bg=K8f^2ux7QKn5uKd0r2KY9>-cf&rabSkWY~Za%?+Y z-ev?({w5Sv)Ah*9=a}kb7OJTQr?$=MHp_Dua<9)$<1@BM>77Xhi_)%Jp;Pu;M+`E0K4I<>1fFWD5e?fg2Wu$;bCeHXg))q zjX!SjE>N=oTR_RrBtU}OqZMtyr^$Vg>4k@C&?9qOCSwP;&;O;b&#K?0yZ_w(OYQrA KX8o^v;Qs>($pmiz literal 33280 zcmeHw3w)eqmG^nynR#dOCYfYra!)Tw+qBc>Leiv{KnqQqh87!oNm{6i=`@+P(V zrnN*HG#8D2%6idh?KzrLldm-sT?vk%xQlPYvj*=w@g^!3Tvv88gYB2sbAaIU%SAh` zVNw1s^R!83;q~0DL>n2olW3S7G4=HjQ2}_5KyJp=vNxh@hs1s(CX=b^GD(>Khp}y62bF z=btHK+?ov)AFV4_oy-qvb^ARm$H0+eT*-xN*id++)_%^hpL6Z!Jo{N^Kj-uFhiT7o zHF%C|>7H?}r2F0DTuJwP#<`O2H^!xs`1RxHlS}++zy|pB0t7D^?e7J&-NPyku}69L zpzU35!x&>I-fid$FJSd!+^8YY3IlDpIJ{8!Y=kk!(1Jrvj9$I)*(hU-q1C|r@^GW@ z*+^rIq16N+qbD}M54Q2EW@TyO)|`=KQ_nJzVxx?tEuAscIuBKxanIOkA)}<(88fYx zEOy39YcaDIag4Y%nRbYg9>mCubd0cNZze|A)ISv?brs6V1wp-Te?G?&11FXkw_~nS z7k3v1U3L30*{g44;K(s<%T?*({*XK9uG?S8f(#r%#%;lBUB)Bi33}@GBmApxWZ(!g zJ|&7<(H)NFHhyO2H?t9?!p+QYkHldSv`m@bEN+Z3%$?s{-w@r|^Sk8UXIJM$g;bnA zzq$4~=J#?fdwz5MH#2su@K23hdVVi~FneaH{aj{0m)p;F`+2_oyug00;3xY`r%p8u zC_VXEC)~;^G5J{|Pkz?Olb<#6<+;+As`_w~)>9Pa#^$vNzeH1RhFJ)q4BcQbjV9|eDdnIqsUPN`}Ki9vq zoc>j-?p2?oHRaE@{ui`YW{E$~*%0T^oqtI?XCJr!QSJTLk6XnXj@$oRf9iyE&gZc} zkit0KnBxHMAeD||rr>#4kzpj=dYcOOr038!ohT1=^<*zkuB`y3tI=;H-mBAawY3d& zLtp|craSR4I7ad^u4*Jsri)p4wu^-?SHe|Oh4a0MSVo|1qJjmEfupn)EPs+0gv1>p zBQ2gq=5Iyjo7sA#q1UqX+#oVuciNb;iD@#sMW+1?W!8wy71=VCZ1eTtI5y7~nPYD# z^BM-moA{#0*!`NO=O0Aoi8qw_JCV^mS^b*9_H=;b*z;bIxk6;5tUpI8RujDb9BB)7xHx=AjvK%4U?aKLlBAdFQ%TNuh5VJ=a^ydHVz zCe;kN8qS-TgEDUF{M_1a&(!7;RlB}%0;{BmUyv()MMj)UY|C)Obi1J&8^vtP-oe5! z58dDr3vHS67d$prRg*~NP@ZE=ZeYaR%&<5>L=!doWMpiTiRl2Gy!QY37ag4eQ37K zFv~zKWVnPksM*E)xfps-%q3w=s`@1Am`RkFN#sS|_hJf}Va%cWMZb~IwMM{@z)~0w zyTW)-6vi%77@KY#Kn>18dU`KcUY@FnzF9Zm@kJ8 z>q@j~jRD|R(sfpLg}c$4s6*{~Pof=?s#}>I{wn!|G%cRXcqAGSDXvA&-(j-KWMv zm-MaC<1X9Vtl2$D!UIN~%R(YRJFpyiA2^oHw9^Keh;nejL0-zgd=Fri*6^#3|M_s2g(EuMN1ZYZ>);U5oWo z-A2$8z7Dkb(zkc};&~zt?z_MZ8mi4AZ~5irveJ`BMb}cY_z?yFotdU8KPf zo#xS&1M1@ijsZAMUUm%VWs4^2(F1*A5Kz{XeL&6_7$;$YGz>#tBZ`F;l3*$6YBUgeXGs!5WT%JznDhUk20 zEKP8GX@Z+f6WnT=;D#My(S*t?AAq(=o=J+lg-Q0yBJmk^8x(}ku4-e6TTxi^sz!80 z<0TV|!Le=w$E;syShoYgg;4l|c=Grg4J9H0)`!3f>egT5G2c)z@cmP!(O?)V0vuoK zG?SKW;ccoqd#~Up$LJQQ6JLGt5H zyPsf$$3c^iH%l-MkYO%oqk9~&Pbx8qm30V=Ql4L9o8Sb~Xor8*rm$h@L?u|*lUMO0 zEQi^KS~(pxRLMyQ2clVARv<^12G37k=E^l-S)W21W!9$wIg5V=PY$%t;@NnSURGzy zAzSy3BTU@7I%;cr*pK=1lP^JuKE?(4`eA;YDV>j*te4ugpyzYYBRki|O<_zUW3mEb zVay?8l5GxS0vVHRb{MnAn5ZYXP;39i=}ztdr@#Il*APBKV`S5Qsp3ch)qzt^7c$WiT zfSxT?4seD4jx})Jb5jC>Wil>3FbWfIgHgV{e9hCS5?%^hm2dg7RGiD#tIE3_;$5z! z2i)WR{8T3a>>P^Nz17gHk8wc=Mf}Y2FV}-6_OGswZ3Tsc!8j6%8{{^Aj4=_$j4{UF z!SrEoP34fw3D0ckYz z1b+xP>YiZMmrn&T&V@naOj#>m1iu^1XZ0}*nY+CRW229~6{uS9p_B>ikvgzP3Kawj zW?xWYeH}IG%03t>3_lDk4`n+) zKWHlF=Z8WVfD)x5R2nF?xP`{bK&k9R#Y4#-f>jqPJF6Xm(AkX$gaReX0(;=jKm7XZ zub*}7^s%?0(`C)Bl6qt_#s|YsqRHe_0OLDABgUHM8Wk5H4hR9sthz?u{4ge!S?>uy z!*UT@Zj&8J`WSPpe?VE8^^bs|%3x*qBqJ5UitvwsGC?JH88CC1Cvobi?5+!tTW>@XM_K!!6AY&JI7v$gJS3@M$3EMMb(7v&MI` zHq{F(j6v?jKVfp}V_XoLr619a+O-H-|+J^NQUcJ|E-Mcxe{Dv14{~dGhCw2+a-7 zO?(EkE&K~m!MSz~tkLzsx#52W57S5b!lRk|C4*l9T$$CWI{ECXEX*_4dIdl+JmFsp zZlBKrRo1_O4$Vju`8e;q3Zk(rFk|+Z_3w=41AUvI^;&soW)eGKqfVJ)%~QD8!Tqx4 zGXomK@|?+^--52@&ufkRpS%Y%328K>k8w;kP5A$q1Q@Vur10^z3RFDSdg?|HcPj&nSI=tcELIpb5QJ~2h z9@L1|qla(96Eh(FD$wPgkIM1H%;qzq29&-Cxj3G6Ag~t5i~0CSfZl~?IfS$IjDS80 z{_pTatf%!n=O$_fUF^x#(+QgSsVV$Q$X|~(K5FpEN4>b+Zs;At`++HZOeuN^I)0eN z=UQI8e+u5qcy|F`p!O!caJeh|AM6|4OzZ(AcNc*6GN(C48C+{eVK&W_|}7PyX06P*8@iQcWT&Zb zTT^pWYjbN0axLL|Y=eMbnoTqp&CjdB7wM{j_oNcB`0gZ297URK^MSXuhrYB%`F`%j zTi17@>|3Ba3sJUu(Avquc)`coWgq!yeI5dUey_Fih7BP=@4%bKnPcHJ-BW&DOIGA67Gc>=F(yfwkgpz8~llo<4?z)b}YhKeWw z5rZBsIOaF#$Ax*t1}zi#Y60T`!S4}xOgP)5?veu5^WO4EEg}===Gs;S(Z1yJ%*qL2=Q4gNykdom+WS zWByU${77g1Y0-I?)V(a9WqxZeDY=#EF&zww6|>BTgG)+!=!3#}m;a_J*oj(x`jGir zv7ZX67+we5pigN_O1@^-eV4}8{tR^udJKHnVD5xxJ`I}!()q=V-(JM6%z+Ii z{aR{$#?PGh1P>PZDKGR|aTV2+JW=kar-R(e;VQ26Pa^X)ESyEpnPq`lbc%IUzbEu*$Sv-gKgp#QB3OGUfl)-hsDs^uNJqmC9++_K;z#D#gE|2^2P+3QrpN<0> z)Ktc$*TWKwX&LuoW;NqQu+~p!FbaP9jL3f-wpG!^Cfj+F)E(DY<{iZ>v%ufM!A2j( ztmoK56yc)evk1!eH1#=zUpw*{vH;2+k+M9#djYi>_1f{-MmcGq+A6$8J0f{59Rc;R zN~S8PNy;vgvIY1aCzm~Ha@it$la#413bmNFunF{W(Ycf^Cs%DTKF8n|mN5m%7G$lP zXcImGW2z1|--O$^lu(yo*0$3gI*?UwAB{`dMkyPjNh(DPyU?SX$U=^-rAF^+z%|}3 zz}E%;E5Wbw-UCjF?=yf&-@SmBdG7*1bM%@iiZ}`0l(vqV%Q`%4?xeX3iT7c$MJ>%OvxvuCLU#s>FPj%i>+U=R0S4IbE zE?}d^@D9Oy1W)RX;5;OprL+uqzQ%B~-Uav;7voSmd=Irw@N0Yk$ zOw3szbql4|hh1C2`L!zsxY~UP@Eh*y@UiHkf@gdN-50ziPoo3chq5?#0v1 zD;j-V|2|-~aBlKEgIeRRlX=>sF)$m-r);H7J zt!s3-O^uqfJR0p#RK?w)c6`zFkWk;Cw$eZ5x#&@wnrUwFxakiz^(p^#SU>jG5?J!Zkrk_F97vdHnp`R0P2H^f-S#ydFTg1 zDO>XC=p1hE8^pF0P|aLLu`LDEJddeseeG4hsd)J;Pa&;Rl=QEVE*DDaETX$@ ziggy#vo^&#i|GwXwVm)H`j;Ray<|Mn%ESezfqcP|D6q`qq55MfJUkZpLPbDYmnUeiLTuTHiwS zp_zP4CHdR}`z??x9HuYlVb+}ipv8i8IHkmVNp-p|Qc&=v_ zwb;~miWiu(=zN=smMt-7(`uU8k^BV4bB=YnX23d>gaij zT3z)K+|IgrYm2hq6!qgTK?ZGVUPa8DPo5T4b}%$-E~GZ0ZlQ1HjbOXJ!={!S*O`my zIh*=^=nnIIy0?w%-9o2A$3gu{sACnM66zf|MDQ3rmN(aPKJBupXYv+6(z2hTNsa5o%c?H$mRH^o&`3`EcskiB`m^)~tO>K3(Xhx{ZrXB;e zlQ!B^jr$dIfG)SG8{IFOQHt2qtDtt#UYlz7ykhR5q)m-@UNmEL*rp0#%U-(1rhZMY zn1l2ln_3K8hUiwCszB>;y3?laas9!x=suhJqpKi)m>#mJZhXUMKRs(xPq^pgC+TNG zoz{+G|C^%WmF&UO+TQ%dpeAjK%SNc6D^oT?RYG0s<48V0b8Lzu`2f}16i4zwYPBhj zQ4 z)P7?bsGHLDR)YGNUG_y|uIDhiqfsrkr8=RYGq-SQg@w(5QP?1!I2r<-mD$3ve5N27Mwpi`lEwY{1pKMNTbJtjTk zQVpdnvqQY3;j#Cb?yyTA^C>jiEWxLsfbP@@>2 zfvkIXsFbc3{1{+0-7fG6z#2LYI3KOu9crLj$tbmwNebkYR3N9M{U|kX>rhc~1z=n0 zZv3Sq<1+*=!H(}BaEAM3&vR0HSNVIWP0m(r#AmBE;%4;6N3_e=NNX@Dt?>U&GP!C2a%7;$GCfL(}wQI8WV6mqW5cD=7FZU~APE0IQ2m zXn8u1Z=T*>bse45zUO~Z`vPh`ubtG|N}tpo1pWu@5qwoJPk&7Crv!hNIzlV;=V@C- zxBd$%DCh%Zoxh;!qPYI5NLK2u>TC2r*!aro`}Ima>Ayx_sQ)Pc-+}+3>NUV0n=@T* z^zIK@rG86!x2saGu0G@{5Kf7}83N}4{@&F9*zRtJ%?7pWuNk*#9s1?|4)-H;bUZFi$*{yeKGyStXom#lMTaV~RtLA!Q`hVmb z_={ogMX&ZzW4Y&k`epe#Pp@dcpMI6U#nYx$RBiEW*6%Rxa`y`UcF6aG^I7v+l(MzG z0@M2F@@(gT#(rR(FMz`>4nQaFjHK>i z(Q_1>cK40K*{}0?;YQ)#PrHp1+DUq~yu?_jb%d&o7VYO)|2nj%j9TE^Dh%q-HW#-U z9oi1TdG-GVap@PRR$onDAg`+n@HW>yG%9fwL0kW5+${aN89jQ*_-pj&kH%fvSIYz5 zh_=0~+Izizs;Jg`v)KRF$OdygCpA~a0`F#RzNgt+slQk})Aec8?eKmK))A;1?aukyyw)-`Brv;PJueV_M) zrePi(hi%8Sd$jBFZ}T44K3eo)@3-~k#$DbAwO7j@^gaSV{J{H|_~9{lpj+RsEi`}S z-EZq$ZT!ajlt?}+lFy6e^CHPJ_nq$jXt&09y?$$GzV8=e|EuEpSH^`Q=D=MaATA_El@0fNdcpd!#{2|{<=uy&lLjO|!)xKBsTk<)Q&yzVjhhu-*vd>2+=qrGa($^vR zxJW*MQXl5iCn5i|$e$GXpNRZvk$(a5J}T6{30SMKe4X|fV59bgua@@FiGn)ZJpVRt z0lvrGUf6`&yAR|y;nwcMfYtOBz`67dzH*&({2ss#(YA*A zsjpzI=-h$tFZUHhgx@dxN#Pt3&eg&>3dse9cMAWw@INN}hlF!NI1l4Yu%Pf^(Ro@p zKNZeTg`;WQ3zx<+E{*$AE1bE)nJb(Q;j9tP8sS8Q)333$M+CoG@S`Y&JsMknT=*Xo z{zJk!A)JSW^PF%_3+JbzlXR}7>0*!0n(=WUwABh{u6{LrrvRVm!NLyV>=5~gNKOiV zMDRNWKQ8z~f}arlv_Q=@20gU`I|OzDE+~u$enjALfhPn$=VGf)3yv>TuvPB&<7=31 z_lKyeutqqm1nzLN&65J}6!?(9=LDV>NFJ8a1l9^4uO*bk9ye7yF4GF#==Jg zKWYfaV9rCv4Kz^jh;U8={x(nZeu#chP%E%QU_{^%FPGlwy@75mI4+zM!Z{86#|4^? zOWlCqDX8(WZMDLgC!AHn>F}}S4#6W*Iw|-O!S59OxX3&t_zB^k7O3TM-P$}EHGvU< zM+6=hcmgs%D|iI>cM47m$89ox1n{?cCj_1rsO5i%UM;B27i|Jha3a8en>Q)=kwUirPQj0hs{wHnzd+M5U|?R@a#N59rQxr^8t0x^}xe`I_O5=ivV@d=K*g3 z)bUAA3-Hx|I<28r;GKXvt)(Tvy8w0ChEH~M+&}QAO$PyWI)t@GqftN|KXbAY_!uD0 zLihw!$N8lL@M`EQr03CcA-#w;{q!^7etH>rJ8hLuc5a~C=rOF6&qD4sI)l%k+?rRb z);4OFX$fsa8`FNQ{hOxiGxhoUQoT#ROh2f9NOH z+dQA~+~;}N^9#?bp4UA8Wc<64=RM#17Vj6l-}L^-tNFY>zwdnC2H${h`E5i?$vw$u zxa`*}I1Tfsw$AG|-#U!vRNkXF8&BoEhMH4(JLR4uSKqgALY^x3FF21+<(0`fIJ@p~ zUsBpAKr9vF?Z>+a(S*|nV#h?}h^d%ca{}BeH!Tw#xMvXBpmzrNQ*8XXSO6c-;dv(D zmgnU`k3282+uX}voPZy?_=9U`?vwf^)djh}Ngt5{wgz$8 zgXi6WyXisg1jY1k(w+Ku>6HFBy{tb0{7Gtd{R8cBouq4B-=sTS-=#-fFVS1vFVR6y zpVnZ+v>zDPYfG`~{3+h0zLT`n`(tefyJN+L?R|Z%EqyJ`wBq7usxubZ9k-IHSby@u zo#}#}Xndd}IU4W3Z~#0=+Lg{#$ChYm>jT!&zkei_h;Fd@_pwUFWCe?ta&d1W(!Xz$ zWexU32c!L|m=$L!RVGr)a-_CIlOuyE5s?BBS)LYwws>;bN~G2%5>`UQq|i3FrR7|` zM1do7E)gXG%OHDBt8G1tm*q(1Sj{3Ma0FdCGB_C7IT+p1LhIMYM~0#ahuWAJh$f-~ zIm|Uyd{=Du2-v-mVEABhac zQlnYCv_4xxnUb=kZYnyI<(tLla?WB}A5XQS*!JSN1Z@fU2UyyRIsQRG+}aWyK+yE3 zE?k0OP!8kAuD-sGxD_8AvPP1E9f?F_bbUOQ;-Tw_T^U`8N_Jc4st$h2+82#;Yb-rA0$O%t zbhIoxmklVe+c}pA_=zY|M?4wjrnthgzP^Kr$nY}S9ZmIZ?d@8gRs!s-$&w|e8rZr+ z>ZR+ppQA4LNqA`_xdy-g#wLK#Z4HbJMlYlln-Z}DkyLd35W-^!kq2*D@y=)}5*tij zNK2M9cXTdZvV8U0=H|Ag%UahgUAB64^OCNv*4CEAOS{&tU9x6r*V5)StJ{{ZUftEa zymiTvwM*A7?P_UX-PPIA*-2~GZ`wqCFtMLfv7x9nlIn@}Q_pA;^P*{uHE3t%WYfjb zcr+11k+UMumS_ZPgZkQ-Ob41iL+COjD^X*3% zgjEy#yfM)+Fu?gGn!pdZ?TZe`fUHj{Jp-83NN@cqhm_Q~HJ*&@if)W^*Eqx~#-dB$cj#XpJFZUWxJ*9J)4fVXUvOD;6CDmlv|khT`Y&>C7wq*TV#vMJ=v^YYj_>D7}+eQ zv-GNdaDZI6udi=4HVF7mqV4T2Ya~7}ot<&`^fJk;)7ib8RyIXO2d&7!B}wc%rnR*` zd3HXQD@hdV5TMaS@@y6DQJh|c6$p`@nqFXcdSiTRJa%Y$@tR2gp6EbF>a3+a9c5C8 zcZfqmu<1 zh@zQjFL=^rwm3R~+Htj2-4NRq9J*rB%?^F zgHe7eUqlIsbz@@dFcJftl8Ex7GrDtR_wHz7b;3F*-V(F1HnjBhiP4g8V}sFzEC#58 zs6<}dIg+l-KF;u*ZEl7$t%*!g2NK=T&cV^%SSnk`G=FAl^hOeJUl)E(HG0rW?8_85 zt*na;Ai-z&P7__|!!}G`ME&%__3>S@4xlhHcupxNj%4a&lx&Ia>cQ%h8r_1`JDK6j z?ryDG!7qiqSw*}q#ncqU7)1`LOr5Ff7`-WhEkS=OQ{Z@4qTx_hfm1_P$M|Tbh8?8B zPsMh|_%x6y?Kv2Y4zJ(UIif;HawnyOQDr>E>`ja!Ln!HoQH_6#%^=M}_*^jKn9B5e^@3#cE_Z+s3O}6J*s@GDR4C)!#nUrs-Q+Fh_N2bDt zXnZ%&wef-EWicQK*+fdATN5!F!8^GxHY|t2HG87{`zY&F>P!k=zNjyTHC`~DlYBC= zw*?!JE)$VKvWELEmWh(+MK0Qykn^0Pv@txt(D`4UG&r2Qk)%zC$1OK)}dxC1e4tj=+xfu$(b z*SAw$Z^$;m-XEaj2#`cJOD*CFENZ(VxHpnB#IS`t8y!$l;t2Hc!6KF9NJz7?!U$TO zT%UFS0xoiwoHu~-uB$t82y*uBAh{`;Kq5SVG7j`~PE4o?nCtxX6%bZ$l@VpXWdCBO0%x)xL!7iEmba~*W&FXZ)b46vyd&RD`5>ecp|zBnPX4SzUV=Rm6K1@ zMl#*7lcRZ}@pgc_8pF{=W@XlFAlWw9Q^Vd~V1D9kYs=b`g!-^A=JSy#a3%`%asEtG zoR!lvMwMkqE>v@e`iA7lB>n4-#j!U@+s!i`=jkEju>*EvSrd%<_C-fI#MAQWrK^dT zgCt4DS4K{mgXn>rX9t`U7$?y*&MR~z7FRWMCMmBvST~&WnR1|WSA>t0*aMmAOMQEA zT*{)SpVK84*T#7t){U@_;Rwu;y*Mj2op4Pr9a)J!IVD~x1eH1$KarKkcZ0|Da<8IV(f*mA>_3lv)@Sy7!PPkrB5##ckP0u zau1V9WRkc@h7-rawTG~K;nM=PA7Y%HGc&sdj2D8*K9~ubId!Sk)0TiCHU=APtk>FL z9gHS8?y)tDr%3LnId=?#?6+@JsgL(BIUJGe7#NDhHw{MOX}QQ$j>uh%`?@{qa+dmV zTuKvCy208Vi#w%*S%j3XJ!Cm$(KISCCl^x6V66TrT!F1Y3<9cxK3uL(V~Wboc+!y> zNTX_WZN3syt5aGuhvd6C_%LOcu8;R8~Bv^x+mK60xylj;IR^*QU#M0Gry(Yq%aUp?xa zN3ipDF8Ab`8s`o5+-VJriZ*t!`8pC(fV-&&P-Nx`4Z86 zf*%px!)POcw$=hB1kY$|O-oxej6ZL*&=Lkd-CCPn&VRHR!FvQ{Yalg{)8-OL*`4fA zeRMQARy+OLf_nR*g)Po*C(BJoLQ2J27@vZ{v7DwHMeD$ys({_n4gOS@IWA$xXV-B$ z&trT5-Q)2ckZw*L7RL>1@p=C+q}b)S<^~*sGgRG-)7Tws%LbJ7xa?ae~G3AyU}h0{o3=!y~)2gEk_HRFvs3htyZ>tH+&R7 zt9G7zJnC`rrSdT4^GK*V)bi)l=gC2Hvuhj_-2>2fQ2fR1r1;lBrg)V{ZkLQ?6yG^n zitm~@z9lcmoQ1FEyiuLBbmcim?7SxUnB(M4j_^f1CNbE7132noQv=%*5_LQ;8Gpm* ztKO7TZw+cV^{D+nt+liR^NwRaj+{G)7T7A9Ie;g}<8Jhi$Bxr$+9%#lx)gtPO?CYe z$0N|NOX~70ZUm+3s?pbqI>Frq%_ES(SS^U8TYSU*&K(kKRQBgl$c$*(k~(hDNylvO zS*1E0&vWAMhaH>)Q}~V%&v?g&Z)i(9$F!18im!IdSUHjP203G467B7TFFEsa!e>98 z)k+uso+-r30Q-8kEjxg(ld<)~_==f&Doa?C%BknkUTEiecL6eFKW05g7xy%UHI;MC zE{w@?(gIVOaFq>V;2+QB#X>CQgcHc`SMLxhl;=WK>Zy*Ua|VCcdWhfJqujp)pR0y650F5B zNxz^g8TBw4j53@l?3o^eIg`hHr9qz_qLAh?LWH+wdNqgTEe%e-q=oQhC$cebjn2aO zBA=9SW4;g>UeN#m1N?eO%OjURIQfcy=8WRfU;z||w2&Thh1?;0DKVtu?ZVrQwLJ+V((%h|ArE>}EVf856k1XmEP;_36GBGFi>3{{y=C0| z^kTay!+zrbhA~Zp9y0WhSI6&Fmj*ZM{+WJuXkhYp2I6V*bfKb7v-5T3>OZ;-6u7-w zpbg*M3QQjNHf#84h*|ilIy8Qmqz=$ij-S1~#`vXVvd=4Y8#)ZM)ez?85 zUq@djkGTA%>8a7R85KrpFrxeM^H45GKuStQTvL)h(-aw>=|iUjqgs&bjN{>}A*L~q ziVa&r#D*^SmIfvthXh9eV_?JB>FArwucs34dr$!6T)$#X27{+ zW@*7<{1ReXH{0dQGvNZUIw-B!1c#5`RQ+IE*o!&+C`$K3i53=PMy0#!gI3Xy)knD7 zF&Su1fj@^bonHD(zqybng#?y49nMsZsDh#zL&n>gU`;%G(cL#$9Z@FL^WY=@EckO~ zGk4<8QNi)&^O=Zr_`43;4AT9W5T@51Tww+#PX<@G@oP_h4{2txiRpqk4X*G>Dk%w(;7y)H_yt$Y_Igd94-|&sGxmT5SE#WHt_ZFu<~VdpIM&&r89~e( z9%3|)fY#80%$aMzxx%*MilQ2%s}O6sM)C&ctM22ruYkC(fO7+ek6*q}jW5U1oJRcY ztBeRbjvktoUIb*uj!xT*&LDn`O7j)_?8ZBDtnGBH?L;~aiY$`E3;5zPp!|0l>QNij zRpcT*I9e6yGhO()ZGqPfcikSC{6KK>4x}9*x~B$uZa15Cos&)REX`{|S*F_3?gSEcA>n2llMrsK=}N~@An2L?+Z@e4~Y|4N%;8{e)>hv zOuwIJ{0WUUd<8J$5TK6=8;Ymnfyr;fcH73~d5Epxk(I$C9yJ^M`JAg@8;>^koUK&6 z>(e|om@5{Ch~o>H^+*ST6kG(|Ous8|WId;j$$MO;=7(N3C(jNgPJ(PIR!RI1zy64vg8;9zl@6+1dPn z=T+VTBJ#l_L-<2h=^TpFz7I^}6l510yhzO=c*+M$j_lXaEN>%1RHS@xM}`YOk8Vet73&T%>e7Xvo~4+DdN7ju2~ zR?Vj#ydeRlVqW$x1;Vf=E=DD|oxxB6VGgU^h+ju`*3d!xXcYDyNazSFLf+sUAw2@u zSBh|zEkzC&!|BIcQ_Y$_@}L+#Kt2oB`0+bJQ0eFFj~NG^?k}Dpjmt{ z`>zEEK2R@&G@JqB#U16O@kHm~U^hO|qal24D-n&# zw>`M@^*ZRlZ{008m#SRF|4aRU&2j<+ZhyZ=goq|#gHmTcld{5^rnl_0{Gd{sk z|Jrct10UdbE=<(rn-;z~=KHl#^e6%nW9jW(a}8o3R}My8ZE-l#3*NB-qUzIo#N zq*Rubgtn*q@tyM5h`Dv9dL-kIdiW+vOQ{ubC*q?S?|#rb@os^QZMa2ShSC9GEU_5n zZFsi;Z$(+V)M*6|zjBCogxw83k4Fc_fbY?UAUg;T)gU5sM Date: Thu, 28 May 2026 21:23:47 +0200 Subject: [PATCH 065/202] fix: update endpoint roles to use Autopilot.Read --- .../Endpoint/MEM/Invoke-ListAndroidEnrollmentProfiles.ps1 | 2 +- .../Endpoint/MEM/Invoke-ListAppleEnrollmentProfiles.ps1 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListAndroidEnrollmentProfiles.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListAndroidEnrollmentProfiles.ps1 index 3cd4218a50fc..ab5df7d31bf7 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListAndroidEnrollmentProfiles.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListAndroidEnrollmentProfiles.ps1 @@ -3,7 +3,7 @@ function Invoke-ListAndroidEnrollmentProfiles { .FUNCTIONALITY Entrypoint .ROLE - Endpoint.MEM.Read + Endpoint.Autopilot.Read .DESCRIPTION Lists Android Enterprise enrollment profiles and hydrates token fields when Graph omits them from the list response. #> diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListAppleEnrollmentProfiles.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListAppleEnrollmentProfiles.ps1 index feb5afc8d9d8..8755764cf5d2 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListAppleEnrollmentProfiles.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListAppleEnrollmentProfiles.ps1 @@ -3,7 +3,7 @@ function Invoke-ListAppleEnrollmentProfiles { .FUNCTIONALITY Entrypoint .ROLE - Endpoint.MEM.Read + Endpoint.Autopilot.Read .DESCRIPTION Lists Apple Automated Device Enrollment tokens and enrollment profiles. #> From 3f03634f66cfaf0abd9fce128e5f6e0167d68c8f Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Fri, 29 May 2026 08:11:21 +0800 Subject: [PATCH 066/202] Update Initialize-CIPPAuth.ps1 --- .../Authentication/Initialize-CIPPAuth.ps1 | 215 ++++++++++-------- 1 file changed, 116 insertions(+), 99 deletions(-) diff --git a/Modules/CIPPCore/Public/Authentication/Initialize-CIPPAuth.ps1 b/Modules/CIPPCore/Public/Authentication/Initialize-CIPPAuth.ps1 index 9f618899c237..9974d0a4a6be 100644 --- a/Modules/CIPPCore/Public/Authentication/Initialize-CIPPAuth.ps1 +++ b/Modules/CIPPCore/Public/Authentication/Initialize-CIPPAuth.ps1 @@ -7,7 +7,8 @@ function Initialize-CIPPAuth { Loads SAM credentials from Key Vault (or DevSecrets table), auto-patches redirect URIs on the SAM and SSO app registrations, and configures EasyAuth if SSO credentials are provisioned but - EasyAuth is not yet enabled. + EasyAuth is not yet enabled. On a fresh deployment with nothing + configured, requests Craft's setup wizard mode. #> [CmdletBinding()] param() @@ -19,26 +20,41 @@ function Initialize-CIPPAuth { NeedsSetup = $true } - # 1. Determine Key Vault name + # -- Entry logging -- + $EasyAuthEnabled = [Craft.Services.AppLifecycleBridge]::IsEasyAuthConfigured() + $IsDevStorage = ($env:AzureWebJobsStorage -eq 'UseDevelopmentStorage=true') -or ($env:NonLocalHostAzurite -eq 'true') $KVName = ($env:WEBSITE_DEPLOYMENT_ID -split '-')[0] - # 2. Try loading SAM credentials - if ($KVName -or $env:AzureWebJobsStorage -eq 'UseDevelopmentStorage=true' -or $env:NonLocalHostAzurite -eq 'true') { + Write-Information "[Auth-Init] Starting — EasyAuth=$EasyAuthEnabled, DevStorage=$IsDevStorage, KVName='$KVName', DeploymentId='$env:WEBSITE_DEPLOYMENT_ID'" + + # 1. Try loading SAM credentials + if ($KVName -or $IsDevStorage) { $AuthState.HasKeyVault = [bool]$KVName + Write-Information "[Auth-Init] Credential source available (KV=$($AuthState.HasKeyVault), DevStorage=$IsDevStorage) — attempting SAM load" try { $Auth = Get-CIPPAuthentication if ($Auth -and $env:ApplicationID -and $env:TenantID) { $AuthState.HasSAMCredentials = $true $AuthState.NeedsSetup = $false $AuthState.IsConfigured = $true - Write-Information "[Auth-Init] SAM credentials loaded (AppID: $($env:ApplicationID))" + Write-Information "[Auth-Init] SAM credentials loaded (AppID: $($env:ApplicationID), TenantID: $($env:TenantID))" + } else { + Write-Information '[Auth-Init] SAM credential load returned but env vars not populated — credentials not available yet (expected on fresh deployment)' } } catch { - Write-Information "[Auth-Init] Could not load SAM credentials: $_" + $ErrorMessage = "$_" + # Distinguish "not found" from real access errors + if ($ErrorMessage -match 'SecretNotFound|not found|does not exist|Development variables not set') { + Write-Information "[Auth-Init] SAM credentials not found in storage — expected on fresh deployment" + } else { + Write-Information "[Auth-Init] ERROR accessing credential storage (possible permission/network issue): $ErrorMessage" + } } + } else { + Write-Information '[Auth-Init] No credential source available — WEBSITE_DEPLOYMENT_ID is not set and not using dev storage. Cannot load SAM credentials.' } - # 3. Auto-patch redirect URIs if we have credentials + # 2. Auto-patch redirect URIs if we have credentials if ($AuthState.HasSAMCredentials) { try { Update-CIPPSAMRedirectUri @@ -53,13 +69,86 @@ function Initialize-CIPPAuth { } } - # 4. If EasyAuth is not configured, check for SSO credentials and set it up - $EasyAuthEnabled = [Craft.Services.AppLifecycleBridge]::IsEasyAuthConfigured() - if (-not $EasyAuthEnabled -and $AuthState.HasSAMCredentials) { - # If the central migration app ID is set, configure EasyAuth with implicit auth - # (no client secret). This lets the user log in via the shared app while the - # ForcedSsoMigrationDialog guides them through creating their own CIPP-SSO app. - # Once they complete migration, step 5 detects the clientId change and cleans up. + # 3. Handle EasyAuth configuration based on current state + if ($EasyAuthEnabled) { + Write-Information '[Auth-Init] EasyAuth is already configured' + + # 3a. If CIPP_SSO_MIGRATION_APPID is set, check if migration is complete + if ($env:CIPP_SSO_MIGRATION_APPID) { + Write-Information '[Auth-Init] EasyAuth is active but CIPP_SSO_MIGRATION_APPID still set — checking if migration is complete...' + try { + $AuthConfigJson = $env:WEBSITE_AUTH_V2_CONFIG_JSON + if ($AuthConfigJson) { + $AuthConfig = $AuthConfigJson | ConvertFrom-Json -ErrorAction Stop + $ConfiguredAppId = $AuthConfig.identityProviders.azureActiveDirectory.registration.clientId + + if ($ConfiguredAppId -eq $env:CIPP_SSO_MIGRATION_APPID) { + Write-Information '[Auth-Init] EasyAuth clientId matches migration app — migration still pending' + } elseif ($ConfiguredAppId) { + Write-Information "[Auth-Init] EasyAuth clientId ($ConfiguredAppId) differs from migration app — migration complete, cleaning up" + $Removed = Remove-CIPPMigrationAppSetting -SettingName 'CIPP_SSO_MIGRATION_APPID' + if ($Removed) { + [Craft.Services.AppLifecycleBridge]::RequestRestart('SSO migration env var cleaned up during warmup') + } + } else { + Write-Information '[Auth-Init] No clientId found in EasyAuth config — skipping cleanup' + } + } + } catch { + Write-Information "[Auth-Init] Migration cleanup check failed (non-fatal): $_" + } + } + + # 3b. Reconcile EasyAuth issuer with SSOMultiTenant setting + if ($AuthState.HasSAMCredentials -and -not $env:CIPP_SSO_MIGRATION_APPID) { + try { + $AuthConfigJson = $env:WEBSITE_AUTH_V2_CONFIG_JSON + if ($AuthConfigJson) { + $AuthConfig = $AuthConfigJson | ConvertFrom-Json -ErrorAction Stop + $CurrentIssuer = $AuthConfig.identityProviders.azureActiveDirectory.registration.openIdIssuer + $ConfiguredAppId = $AuthConfig.identityProviders.azureActiveDirectory.registration.clientId + + if ($CurrentIssuer -and $ConfiguredAppId) { + $SSOMultiTenant = $false + if ($IsDevStorage) { + try { + $DevSecretsTable = Get-CIPPTable -tablename 'DevSecrets' + $Secret = Get-CIPPAzDataTableEntity @DevSecretsTable -Filter "PartitionKey eq 'SSO' and RowKey eq 'SSO'" -ErrorAction SilentlyContinue + $SSOMultiTenant = $Secret.SSOMultiTenant -eq 'True' + } catch { } + } elseif ($KVName) { + try { + $MtVal = Get-CippKeyVaultSecret -VaultName $KVName -Name 'SSOMultiTenant' -AsPlainText -ErrorAction Stop + $SSOMultiTenant = $MtVal -eq 'True' + } catch { } + } + + $ExpectedIssuer = if ($SSOMultiTenant) { + 'https://login.microsoftonline.com/common/v2.0' + } else { + "https://login.microsoftonline.com/$($env:TenantID)/v2.0" + } + + if ($CurrentIssuer -ne $ExpectedIssuer) { + Write-Information "[Auth-Init] EasyAuth issuer mismatch: current=$CurrentIssuer expected=$ExpectedIssuer — updating" + $Configured = Set-CIPPSSOEasyAuth -AppId $ConfiguredAppId -MultiTenant $SSOMultiTenant -TenantId $env:TenantID + if ($Configured) { + Write-Information '[Auth-Init] EasyAuth issuer updated — requesting container restart' + [Craft.Services.AppLifecycleBridge]::RequestRestart('EasyAuth issuer updated to match SSOMultiTenant setting during warmup') + } + } else { + Write-Information "[Auth-Init] EasyAuth issuer matches SSOMultiTenant setting ($SSOMultiTenant) — no update needed" + } + } + } + } catch { + Write-Information "[Auth-Init] EasyAuth issuer reconciliation failed (non-fatal): $_" + } + } + } elseif ($AuthState.HasSAMCredentials) { + # EasyAuth NOT configured but we DO have SAM credentials — try to auto-configure + Write-Information '[Auth-Init] EasyAuth not configured but SAM credentials available — attempting auto-configuration' + if ($env:CIPP_SSO_MIGRATION_APPID) { Write-Information "[Auth-Init] CIPP_SSO_MIGRATION_APPID is set ($($env:CIPP_SSO_MIGRATION_APPID)) — configuring implicit auth EasyAuth" try { @@ -74,12 +163,12 @@ function Initialize-CIPPAuth { return $AuthState } - Write-Information '[Auth-Init] EasyAuth not configured — checking for SSO credentials...' + # Try to find SSO credentials and configure EasyAuth automatically try { $SSOAppId = $null $SSOMultiTenant = $false - if ($env:AzureWebJobsStorage -eq 'UseDevelopmentStorage=true' -or $env:NonLocalHostAzurite -eq 'true') { + if ($IsDevStorage) { $DevSecretsTable = Get-CIPPTable -tablename 'DevSecrets' $Secret = Get-CIPPAzDataTableEntity @DevSecretsTable -Filter "PartitionKey eq 'SSO' and RowKey eq 'SSO'" -ErrorAction SilentlyContinue $SSOAppId = $Secret.SSOAppId @@ -100,95 +189,23 @@ function Initialize-CIPPAuth { [Craft.Services.AppLifecycleBridge]::RequestRestart('EasyAuth configured from SSO credentials during warmup') } } else { - Write-Information '[Auth-Init] No SSO credentials found — enabling setup wizard' - [Craft.Services.AppLifecycleBridge]::RequestSetupMode('No SSO credentials found — setup wizard needed for initial EasyAuth configuration') + Write-Information '[Auth-Init] SAM credentials loaded but no SSO AppId found — enabling setup wizard' + [Craft.Services.AppLifecycleBridge]::RequestSetupMode('SAM credentials available but no SSO app configured — setup wizard needed') } } catch { Write-Information "[Auth-Init] SSO EasyAuth setup failed (non-fatal): $_" + [Craft.Services.AppLifecycleBridge]::RequestSetupMode('SSO setup failed — setup wizard needed for manual configuration') } + } else { + # No EasyAuth AND no SAM credentials — this is a fresh/unconfigured deployment + Write-Information '[Auth-Init] Fresh deployment detected — no EasyAuth configured and no SAM credentials available' + Write-Information '[Auth-Init] Requesting setup wizard mode for initial configuration' + [Craft.Services.AppLifecycleBridge]::RequestSetupMode('Fresh deployment — no credentials or EasyAuth configured') + $AuthState.NeedsSetup = $true } - # 5. Reconcile EasyAuth issuer with SSOMultiTenant setting: if EasyAuth is already - # configured, check whether the issuer URL matches the current SSOMultiTenant value - # from Key Vault. If it changed (e.g. toggled from single to multi-tenant), update - # the EasyAuth config via ARM and restart. - if ($EasyAuthEnabled -and $AuthState.HasSAMCredentials -and -not $env:CIPP_SSO_MIGRATION_APPID) { - try { - $AuthConfigJson = $env:WEBSITE_AUTH_V2_CONFIG_JSON - if ($AuthConfigJson) { - $AuthConfig = $AuthConfigJson | ConvertFrom-Json -ErrorAction Stop - $CurrentIssuer = $AuthConfig.identityProviders.azureActiveDirectory.registration.openIdIssuer - $ConfiguredAppId = $AuthConfig.identityProviders.azureActiveDirectory.registration.clientId - - if ($CurrentIssuer -and $ConfiguredAppId) { - # Read SSOMultiTenant from KV/DevSecrets - $SSOMultiTenant = $false - if ($env:AzureWebJobsStorage -eq 'UseDevelopmentStorage=true' -or $env:NonLocalHostAzurite -eq 'true') { - try { - $DevSecretsTable = Get-CIPPTable -tablename 'DevSecrets' - $Secret = Get-CIPPAzDataTableEntity @DevSecretsTable -Filter "PartitionKey eq 'SSO' and RowKey eq 'SSO'" -ErrorAction SilentlyContinue - $SSOMultiTenant = $Secret.SSOMultiTenant -eq 'True' - } catch { } - } elseif ($KVName) { - try { - $MtVal = Get-CippKeyVaultSecret -VaultName $KVName -Name 'SSOMultiTenant' -AsPlainText -ErrorAction Stop - $SSOMultiTenant = $MtVal -eq 'True' - } catch { } - } - - $ExpectedIssuer = if ($SSOMultiTenant) { - 'https://login.microsoftonline.com/common/v2.0' - } else { - "https://login.microsoftonline.com/$($env:TenantID)/v2.0" - } - - if ($CurrentIssuer -ne $ExpectedIssuer) { - Write-Information "[Auth-Init] EasyAuth issuer mismatch: current=$CurrentIssuer expected=$ExpectedIssuer — updating" - $Configured = Set-CIPPSSOEasyAuth -AppId $ConfiguredAppId -MultiTenant $SSOMultiTenant -TenantId $env:TenantID - if ($Configured) { - Write-Information '[Auth-Init] EasyAuth issuer updated — requesting container restart' - [Craft.Services.AppLifecycleBridge]::RequestRestart('EasyAuth issuer updated to match SSOMultiTenant setting during warmup') - } - } else { - Write-Information "[Auth-Init] EasyAuth issuer matches SSOMultiTenant setting ($SSOMultiTenant) — no update needed" - } - } - } - } catch { - Write-Information "[Auth-Init] EasyAuth issuer reconciliation failed (non-fatal): $_" - } - } - - # 6. Post-migration cleanup: if CIPP_SSO_MIGRATION_APPID is still set but EasyAuth - # is now configured, check whether the EasyAuth clientId still matches the migration - # app. If it differs, the customer's own CIPP-SSO app is active and we can remove - # the migration trigger env var. - if ($EasyAuthEnabled -and $env:CIPP_SSO_MIGRATION_APPID) { - Write-Information '[Auth-Init] EasyAuth is active but CIPP_SSO_MIGRATION_APPID still set — checking if migration is complete...' - try { - $AuthConfigJson = $env:WEBSITE_AUTH_V2_CONFIG_JSON - if ($AuthConfigJson) { - $AuthConfig = $AuthConfigJson | ConvertFrom-Json -ErrorAction Stop - $ConfiguredAppId = $AuthConfig.identityProviders.azureActiveDirectory.registration.clientId - - if ($ConfiguredAppId -eq $env:CIPP_SSO_MIGRATION_APPID) { - # EasyAuth is still using the central migration app — migration not done yet - Write-Information '[Auth-Init] EasyAuth clientId matches migration app — migration still pending' - } elseif ($ConfiguredAppId) { - # EasyAuth clientId differs from the migration app — customer's own app is active - Write-Information "[Auth-Init] EasyAuth clientId ($ConfiguredAppId) differs from migration app — migration complete, cleaning up" - $Removed = Remove-CIPPMigrationAppSetting -SettingName 'CIPP_SSO_MIGRATION_APPID' - if ($Removed) { - [Craft.Services.AppLifecycleBridge]::RequestRestart('SSO migration env var cleaned up during warmup') - } - } else { - Write-Information '[Auth-Init] No clientId found in EasyAuth config — skipping cleanup' - } - } - } catch { - Write-Information "[Auth-Init] Migration cleanup check failed (non-fatal): $_" - } - } + # -- Exit logging -- + Write-Information "[Auth-Init] Complete — IsConfigured=$($AuthState.IsConfigured), HasSAM=$($AuthState.HasSAMCredentials), NeedsSetup=$($AuthState.NeedsSetup), EasyAuth=$EasyAuthEnabled" return $AuthState } From e0f45f2035c77c7b8916a605a8d2e13ccd1c80d9 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Fri, 29 May 2026 14:41:44 +0800 Subject: [PATCH 067/202] Backup excluded tenants config --- Modules/CIPPCore/Public/New-CIPPBackup.ps1 | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Modules/CIPPCore/Public/New-CIPPBackup.ps1 b/Modules/CIPPCore/Public/New-CIPPBackup.ps1 index ec73257e425c..f1f155657c97 100644 --- a/Modules/CIPPCore/Public/New-CIPPBackup.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPBackup.ps1 @@ -70,6 +70,14 @@ function New-CIPPBackup { } $Entities | Select-Object * -ExcludeProperty DomainAnalyser, table, Timestamp, ETag, Results | Select-Object *, @{l = 'table'; e = { $CSVTable } } } + # Back up excluded tenant rows (user-configured exclusion state only) + $TenantsTable = Get-CippTable -tablename 'Tenants' + $ExcludedTenants = Get-AzDataTableEntity @TenantsTable -Filter "PartitionKey eq 'Tenants' and Excluded eq true" + if ($ExcludedTenants) { + $CSVfile = @($CSVfile) + @( + $ExcludedTenants | Select-Object PartitionKey, RowKey, customerId, defaultDomainName, displayName, Excluded, ExcludeDate, ExcludeUser | Select-Object *, @{l = 'table'; e = { 'Tenants' } } + ) + } $RowKey = 'CIPPBackup' + '_' + (Get-Date).ToString('yyyy-MM-dd-HHmm') $BackupData = [string]($CSVfile | ConvertTo-Json -Compress -Depth 100) $TableName = 'CIPPBackup' From e3d57cf0ae0ef981173205cf0eaa6d5b26e3f371 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Fri, 29 May 2026 17:43:32 +0800 Subject: [PATCH 068/202] Update Invoke-CIPPStandardDeployCheckChromeExtension.ps1 --- .../Invoke-CIPPStandardDeployCheckChromeExtension.ps1 | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDeployCheckChromeExtension.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDeployCheckChromeExtension.ps1 index 01469ff25668..aaa3a2b7858a 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDeployCheckChromeExtension.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDeployCheckChromeExtension.ps1 @@ -30,7 +30,6 @@ function Invoke-CIPPStandardDeployCheckChromeExtension { {"type":"autoComplete","multiple":true,"creatable":true,"required":false,"freeSolo":true,"name":"standards.DeployCheckChromeExtension.urlAllowlist","label":"URL Allowlist","placeholder":"e.g. https://example.com/*","helperText":"Enter URLs to allowlist in the extension. Press enter to add each URL. Wildcards are allowed. This should be used for sites that are being blocked by the extension but are known to be safe."} {"type":"switch","name":"standards.DeployCheckChromeExtension.domainSquattingEnabled","label":"Enable domain squatting detection","defaultValue":true} {"type":"textField","name":"standards.DeployCheckChromeExtension.companyName","label":"Company Name","placeholder":"YOUR-COMPANY","required":false} - {"type":"textField","name":"standards.DeployCheckChromeExtension.companyURL","label":"Company URL","placeholder":"https://yourcompany.com","required":false} {"type":"textField","name":"standards.DeployCheckChromeExtension.productName","label":"Product Name","placeholder":"YOUR-PRODUCT-NAME","required":false} {"type":"textField","name":"standards.DeployCheckChromeExtension.supportEmail","label":"Support Email","placeholder":"support@yourcompany.com","required":false} {"type":"textField","name":"standards.DeployCheckChromeExtension.supportUrl","label":"Support URL","placeholder":"https://support.yourcompany.com","required":false} @@ -108,7 +107,7 @@ function Invoke-CIPPStandardDeployCheckChromeExtension { $SupportUrl = $Settings.supportUrl ?? '' $PrivacyPolicyUrl = $Settings.privacyPolicyUrl ?? '' $AboutUrl = $Settings.aboutUrl ?? '' - $PrimaryColor = if ($Settings.primaryColor) { $Settings.primaryColor } else { '#F77F00' } + $PrimaryColor = if ($Settings.primaryColor) { '#{0}' -f ($Settings.primaryColor -replace '^#+', '') } else { '#F77F00' } $LogoUrl = $Settings.logoUrl ?? '' ########################################################################## From a17137cbe0abe8418d43791ed751300b0ed84723 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 29 May 2026 14:19:45 -0400 Subject: [PATCH 069/202] chore: remove cipp processor queue C-003 --- Config/CIPPTimers.json | 8 --- .../Start-CIPPProcessorQueue.ps1 | 59 ------------------- 2 files changed, 67 deletions(-) delete mode 100644 Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-CIPPProcessorQueue.ps1 diff --git a/Config/CIPPTimers.json b/Config/CIPPTimers.json index 0802c9863096..745034562ce3 100644 --- a/Config/CIPPTimers.json +++ b/Config/CIPPTimers.json @@ -17,14 +17,6 @@ "RunOnProcessor": true, "PreferredProcessor": "usertasks" }, - { - "Id": "168decf3-7ddd-471e-ab46-8b40be0f18ae", - "Command": "Start-CIPPProcessorQueue", - "Description": "Timer to handle user initiated tasks", - "Cron": "0 */15 * * * *", - "Priority": 1, - "RunOnProcessor": true - }, { "Id": "44a40668-ed71-403c-8c26-b32e320086ad", "Command": "Start-AuditLogOrchestrator", diff --git a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-CIPPProcessorQueue.ps1 b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-CIPPProcessorQueue.ps1 deleted file mode 100644 index cbc065c4f15c..000000000000 --- a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-CIPPProcessorQueue.ps1 +++ /dev/null @@ -1,59 +0,0 @@ -function Start-CIPPProcessorQueue { - <# - .SYNOPSIS - Starts a specified function on the processor node - #> - [CmdletBinding(SupportsShouldProcess = $true)] - param() - - $QueueTable = Get-CIPPTable -tablename 'ProcessorQueue' - $QueueItems = Get-CIPPAzDataTableEntity @QueueTable -Filter "PartitionKey eq 'Function'" - - foreach ($QueueItem in $QueueItems) { - $FunctionName = $QueueItem.FunctionName ?? $QueueItem.RowKey - if ($PSCmdlet.ShouldProcess("Processing function $($FunctionName)")) { - Write-Information "Running queued function $($FunctionName)" - if ($QueueItem.Parameters) { - try { - $Parameters = $QueueItem.Parameters | ConvertFrom-Json -AsHashtable - } catch { - $Parameters = @{} - } - } else { - $Parameters = @{} - } - if (Get-Command -Name $FunctionName -ErrorAction SilentlyContinue) { - try { - # Prepare telemetry metadata - $metadata = @{ - FunctionName = $FunctionName - TriggerType = 'ProcessorQueue' - QueueRowKey = $QueueItem.RowKey - } - - # Add parameters info if available - if ($Parameters.Count -gt 0) { - $metadata['ParameterCount'] = $Parameters.Count - # Add common parameters - if ($Parameters.Tenant) { - $metadata['Tenant'] = $Parameters.Tenant - } - if ($Parameters.TenantFilter) { - $metadata['Tenant'] = $Parameters.TenantFilter - } - } - - # Wrap function execution with telemetry - - Invoke-Command -ScriptBlock { & $FunctionName @Parameters } - - } catch { - Write-Warning "Failed to run function $($FunctionName). Error: $($_.Exception.Message)" - } - } else { - Write-Warning "Function $($FunctionName) not found" - } - Remove-AzDataTableEntity -Force @QueueTable -Entity $QueueItem - } - } -} From 0fd3315b9d405ed4d459ac3c8f740a640fb74099 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 29 May 2026 14:31:14 -0400 Subject: [PATCH 070/202] chore: disable cippcommand action previously removed in UI, not needed anymore C-004 --- .../Webhooks/Invoke-CIPPWebhookProcessing.ps1 | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/Modules/CIPPCore/Public/Webhooks/Invoke-CIPPWebhookProcessing.ps1 b/Modules/CIPPCore/Public/Webhooks/Invoke-CIPPWebhookProcessing.ps1 index 6ca3b39290fa..aa2ddec7fcea 100644 --- a/Modules/CIPPCore/Public/Webhooks/Invoke-CIPPWebhookProcessing.ps1 +++ b/Modules/CIPPCore/Public/Webhooks/Invoke-CIPPWebhookProcessing.ps1 @@ -79,7 +79,7 @@ function Invoke-CippWebhookProcessing { "Completed BEC Remediate for $Username" Write-LogMessage -API 'BECRemediate' -tenant $tenantfilter -message "Executed Remediation for $Username" -sev 'Info' } - 'cippcommand' { + <#'cippcommand' { $CommandSplat = @{} $action.parameters.psobject.properties | ForEach-Object { $CommandSplat.Add($_.name, $_.value) } if ($CommandSplat['userid']) { $CommandSplat['userid'] = $Data.UserId } @@ -88,6 +88,9 @@ function Invoke-CippWebhookProcessing { if ($CommandSplat['user']) { $CommandSplat['user'] = $Data.UserId } if ($CommandSplat['username']) { $CommandSplat['username'] = $Data.UserId } & $action.command.value @CommandSplat + }#> + default { + Write-Host "Unknown action: $action" } } } @@ -146,12 +149,12 @@ function Invoke-CippWebhookProcessing { } 'generateWebhook' { $CippAlert = @{ - Type = 'webhook' - Title = $GenerateJSON.Title - JSONContent = $JsonContent - TenantFilter = $TenantFilter - APIName = 'Audit Log Alerts' - SchemaSource = 'Audit Log Alert' + Type = 'webhook' + Title = $GenerateJSON.Title + JSONContent = $JsonContent + TenantFilter = $TenantFilter + APIName = 'Audit Log Alerts' + SchemaSource = 'Audit Log Alert' InvokingCommand = 'Start-AuditLogProcessingOrchestrator' } Write-Host 'Sending Webhook Content' From e98445f25614437873439fe27a2b0f0e95d5dc69 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 29 May 2026 15:07:33 -0400 Subject: [PATCH 071/202] chore: sanitize cippid in public webhooks C-001 --- .../Tenant/Administration/Alerts/Invoke-PublicWebhooks.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-PublicWebhooks.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-PublicWebhooks.ps1 index 1dce61f36442..aa8be40e3db2 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-PublicWebhooks.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-PublicWebhooks.ps1 @@ -25,8 +25,9 @@ function Invoke-PublicWebhooks { $body = $Request.Query.validationCode $StatusCode = [HttpStatusCode]::OK } elseif ($Request.Query.CIPPID) { + $CIPPID = ConvertTo-CIPPODataFilterValue -Value $Request.Query.CIPPID -Type Guid $WebhookTable = Get-CIPPTable -TableName webhookTable - $Webhookinfo = Get-CIPPAzDataTableEntity @WebhookTable -Filter "RowKey eq '$($Request.Query.CIPPID)'" -First 1 + $Webhookinfo = Get-CIPPAzDataTableEntity @WebhookTable -Filter "RowKey eq '$CIPPID'" -First 1 if (-not $Webhookinfo) { Write-Host "No matching CIPPID found: $($Request.Query.CIPPID)" $Body = 'This webhook is not authorized.' From 38e3ae9e070ba05968a07647d137efc0ae2491b3 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 29 May 2026 15:27:21 -0400 Subject: [PATCH 072/202] chore: block arbitrary cmdlets not in CIPP modules address C-007 --- .../Push-ExecScheduledCommand.ps1 | 15 +++++++++++++++ Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 | 13 +++++++++++++ .../Tools/Get-CIPPSchedulerBlockedCommands.ps1 | 10 ++++++++++ 3 files changed, 38 insertions(+) diff --git a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 index 403da6b88d0f..5fa07451f809 100644 --- a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 +++ b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 @@ -161,6 +161,21 @@ function Push-ExecScheduledCommand { } } + $Command = Get-Command -Name $Item.Command -ErrorAction SilentlyContinue + if ($Command.Module -notin @('CIPPCore', 'CIPPAlerts', 'CIPPStandards', 'CIPPTests', 'CIPPDB')) { + Write-LogMessage -headers $Headers -API 'ScheduledTask' -message "Blocked attempt to schedule command from unauthorized module: $($Command.ModuleName)\$RequestedCommand" -Sev 'Warning' + $Results = "Task blocked: The command '$RequestedCommand' is not permitted to run as a scheduled task." + if (!$IsMultiTenantExecution) { + Update-AzDataTableEntity -Force @Table -Entity @{ + PartitionKey = $task.PartitionKey + RowKey = $task.RowKey + Results = "$Results" + TaskState = $State + } + } + Remove-Variable -Name ScheduledTaskId -Scope Script -ErrorAction SilentlyContinue + return + } if ($Item.Command -in (Get-CIPPSchedulerBlockedCommands)) { $Results = "Task blocked: '$($Item.Command)' is not permitted to run as a scheduled task." $State = 'Failed' diff --git a/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 b/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 index f9a5c0159923..3de8266ed5f3 100644 --- a/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 @@ -63,6 +63,19 @@ function Add-CIPPScheduledTask { } $RequestedCommand = $task.Command.value ?? $task.Command + + $Command = Get-Command $RequestedCommand -ErrorAction SilentlyContinue + + if (!$Command) { + Write-LogMessage -headers $Headers -API 'ScheduledTask' -message "Blocked attempt to schedule non-existent command: $RequestedCommand" -Sev 'Warning' + return "Error - The command '$RequestedCommand' does not exist and cannot be scheduled." + } + + if ($Command.Module -notin @('CIPPCore', 'CIPPAlerts', 'CIPPStandards', 'CIPPTests', 'CIPPDB')) { + Write-LogMessage -headers $Headers -API 'ScheduledTask' -message "Blocked attempt to schedule command from unauthorized module: $($Command.ModuleName)\$RequestedCommand" -Sev 'Warning' + return "Error - The command '$RequestedCommand' is not permitted to run as a scheduled task." + } + if ($RequestedCommand -in (Get-CIPPSchedulerBlockedCommands)) { Write-LogMessage -headers $Headers -API 'ScheduledTask' -message "Blocked attempt to schedule restricted command: $RequestedCommand" -Sev 'Warning' return "Error - The command '$RequestedCommand' is not permitted to run as a scheduled task." diff --git a/Modules/CIPPCore/Public/Tools/Get-CIPPSchedulerBlockedCommands.ps1 b/Modules/CIPPCore/Public/Tools/Get-CIPPSchedulerBlockedCommands.ps1 index e46e3e82cd91..9ed724c78ed6 100644 --- a/Modules/CIPPCore/Public/Tools/Get-CIPPSchedulerBlockedCommands.ps1 +++ b/Modules/CIPPCore/Public/Tools/Get-CIPPSchedulerBlockedCommands.ps1 @@ -18,9 +18,17 @@ function Get-CIPPSchedulerBlockedCommands { 'Get-CIPPAzIdentityToken' 'Get-CIPPAuthentication' 'New-CIPPAzServiceSAS' + 'New-GraphPOSTRequest' + 'New-GraphGetRequest' + 'New-GraphBulkRequest' + 'New-ExoRequest' + + # Env + 'Set-CIPPEnvVarBackup' # Az Functions cmdlet 'Get-CIPPAzFunctionAppSetting' + 'Get-CIPPAzFunctionAppSubId' 'Update-CIPPAzFunctionAppSetting' # Extension authentication tokens @@ -32,6 +40,7 @@ function Get-CIPPSchedulerBlockedCommands { # Secret & key material 'Get-CippKeyVaultSecret' + 'Set-CippKeyVaultSecret' 'Remove-CippKeyVaultSecret' 'Get-ExtensionAPIKey' 'Set-ExtensionAPIKey' @@ -42,6 +51,7 @@ function Get-CIPPSchedulerBlockedCommands { # SAM permission enumeration - exposes which permissions the SAM app holds 'Get-CippSamPermissions' + 'Get-CIPPRolePermissions' # Direct storage access - bypasses CIPP data access controls 'Get-CIPPTable' From c18bda8795693324d834f54fe9235959e0e3372f Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 29 May 2026 15:31:19 -0400 Subject: [PATCH 073/202] fix: optimize checks --- .../Push-ExecScheduledCommand.ps1 | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 index 5fa07451f809..fde3ca8e0658 100644 --- a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 +++ b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 @@ -162,9 +162,9 @@ function Push-ExecScheduledCommand { } $Command = Get-Command -Name $Item.Command -ErrorAction SilentlyContinue - if ($Command.Module -notin @('CIPPCore', 'CIPPAlerts', 'CIPPStandards', 'CIPPTests', 'CIPPDB')) { - Write-LogMessage -headers $Headers -API 'ScheduledTask' -message "Blocked attempt to schedule command from unauthorized module: $($Command.ModuleName)\$RequestedCommand" -Sev 'Warning' - $Results = "Task blocked: The command '$RequestedCommand' is not permitted to run as a scheduled task." + if ($null -eq $Command) { + $Results = "Task Failed: The command $($Item.Command) does not exist." + $State = 'Failed' if (!$IsMultiTenantExecution) { Update-AzDataTableEntity -Force @Table -Entity @{ PartitionKey = $task.PartitionKey @@ -173,13 +173,16 @@ function Push-ExecScheduledCommand { TaskState = $State } } + + Write-LogMessage -API 'Scheduler_UserTasks' -tenant $Tenant -tenantid $TenantInfo.customerId -message "Failed to execute task $($task.Name): The command $($Item.Command) does not exist." -sev Error Remove-Variable -Name ScheduledTaskId -Scope Script -ErrorAction SilentlyContinue return } - if ($Item.Command -in (Get-CIPPSchedulerBlockedCommands)) { - $Results = "Task blocked: '$($Item.Command)' is not permitted to run as a scheduled task." + + if ($Command.Module -notin @('CIPPCore', 'CIPPAlerts', 'CIPPStandards', 'CIPPTests', 'CIPPDB')) { $State = 'Failed' - Write-LogMessage -API 'Scheduler_UserTasks' -tenant $Tenant -tenantid $TenantInfo.customerId -message "Blocked execution of restricted command '$($Item.Command)' in task $($task.Name)" -sev Warning + Write-LogMessage -headers $Headers -API 'ScheduledTask' -message "Blocked attempt to schedule command from unauthorized module: $($Command.ModuleName)\$($Item.Command)" -Sev 'Warning' + $Results = "Task blocked: The command '$($Item.Command)' is not permitted to run as a scheduled task." if (!$IsMultiTenantExecution) { Update-AzDataTableEntity -Force @Table -Entity @{ PartitionKey = $task.PartitionKey @@ -191,11 +194,10 @@ function Push-ExecScheduledCommand { Remove-Variable -Name ScheduledTaskId -Scope Script -ErrorAction SilentlyContinue return } - - $Function = Get-Command -Name $Item.Command - if ($null -eq $Function) { - $Results = "Task Failed: The command $($Item.Command) does not exist." + if ($Item.Command -in (Get-CIPPSchedulerBlockedCommands)) { + $Results = "Task blocked: '$($Item.Command)' is not permitted to run as a scheduled task." $State = 'Failed' + Write-LogMessage -API 'Scheduler_UserTasks' -tenant $Tenant -tenantid $TenantInfo.customerId -message "Blocked execution of restricted command '$($Item.Command)' in task $($task.Name)" -sev Warning if (!$IsMultiTenantExecution) { Update-AzDataTableEntity -Force @Table -Entity @{ PartitionKey = $task.PartitionKey @@ -204,12 +206,12 @@ function Push-ExecScheduledCommand { TaskState = $State } } - - Write-LogMessage -API 'Scheduler_UserTasks' -tenant $Tenant -tenantid $TenantInfo.customerId -message "Failed to execute task $($task.Name): The command $($Item.Command) does not exist." -sev Error Remove-Variable -Name ScheduledTaskId -Scope Script -ErrorAction SilentlyContinue return } + $Function = $Command + try { $PossibleParams = $Function.Parameters.Keys $keysToRemove = [System.Collections.Generic.List[string]]@() From c69e2ce19d7c8c0df0cd2c4f2de9db936da5c736 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 29 May 2026 15:33:15 -0400 Subject: [PATCH 074/202] fix: allow for command without .value --- Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 b/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 index 3de8266ed5f3..2051ccffce9d 100644 --- a/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 @@ -203,7 +203,7 @@ function Add-CIPPScheduledTask { Tenant = [string]$tenantFilter excludedTenants = [string]$excludedTenants Name = [string]$task.Name - Command = [string]$task.Command.value + Command = [string]$RequestedCommand Parameters = [string]$Parameters ScheduledTime = [string]$task.ScheduledTime Recurrence = [string]$Recurrence From da7bd8c459ab0e75abbcc9a0be96224848a11284 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 29 May 2026 15:43:15 -0400 Subject: [PATCH 075/202] chore: add devsecrets to restricted tables address C-010 --- .../HTTP Functions/CIPP/Settings/Invoke-ExecRestoreBackup.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecRestoreBackup.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecRestoreBackup.ps1 index fb26f73d997c..db67ef64d2e1 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecRestoreBackup.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecRestoreBackup.ps1 @@ -14,7 +14,7 @@ function Invoke-ExecRestoreBackup { $AzureTableTypes = @( [string], [int], [long], [double], [bool], [datetime], [guid], [byte[]] ) - $RestrictedTables = @('AccessRoleGroups', 'AccessIPRanges', 'CustomRoles') # tables that require superadmin to restore + $RestrictedTables = @('AccessRoleGroups', 'AccessIPRanges', 'CustomRoles', 'DevSecrets') # tables that require superadmin to restore # Resolve the calling user's roles, including Entra group-based roles $CallingUser = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Request.Headers.'x-ms-client-principal')) | ConvertFrom-Json From 2ed3f94c91dcfabbc1dbd773c25120bc712801d7 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 29 May 2026 15:44:24 -0400 Subject: [PATCH 076/202] chore: remove write host address M-012 --- .../HTTP Functions/CIPP/Setup/Invoke-ExecTokenExchange.ps1 | 1 - 1 file changed, 1 deletion(-) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecTokenExchange.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecTokenExchange.ps1 index c53b7f1690b7..516ecdc914af 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecTokenExchange.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecTokenExchange.ps1 @@ -68,7 +68,6 @@ function Invoke-ExecTokenExchange { $FormData['client_secret'] = $ClientSecret } - Write-Host "Posting this data: $($FormData | ConvertTo-Json -Depth 15)" $Results = Invoke-RestMethod -Uri $TokenUrl -Method Post -Body $FormData -ContentType 'application/x-www-form-urlencoded' -ErrorAction Stop -SkipHttpErrorCheck } catch { $ErrorMessage = $_.Exception From f5f373681cd6e48c3b27b7140d46115c18f828a7 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Sat, 30 May 2026 12:18:36 +0800 Subject: [PATCH 077/202] Optimize CIPP DB orchestration --- .../CIPPDBCache/Push-ExecCIPPDBCache.ps1 | 15 ++- .../Activity Triggers/Tests/Push-CIPPTest.ps1 | 19 +++- .../Tests/Push-CIPPTestsList.ps1 | 16 +++- Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 | 9 +- .../Start-CIPPDBTestsRun.ps1 | 5 +- Modules/CIPPCore/Public/Get-CIPPDbItem.ps1 | 3 +- .../Public/Get-CIPPDomainAnalyser.ps1 | 12 +++ Modules/CIPPCore/Public/Get-CippDbRole.ps1 | 2 +- .../CIPPCore/Public/Get-CippDbRoleMembers.ps1 | 6 +- .../Public/Invoke-CIPPDBCacheCollection.ps1 | 6 +- .../Public/Invoke-CIPPTestCollection.ps1 | 72 +++++++++++---- Modules/CIPPCore/Public/New-CIPPDbRequest.ps1 | 14 ++- .../DBCache/Set-CIPPDbCacheTestData.ps1 | 86 +++++++++-------- .../CIS/Identity/Invoke-CippTestCIS_1_1_1.ps1 | 15 ++- .../CIS/Identity/Invoke-CippTestCIS_1_1_2.ps1 | 8 +- .../CIS/Identity/Invoke-CippTestCIS_1_1_4.ps1 | 20 ++-- .../CIS/Identity/Invoke-CippTestCIS_1_2_1.ps1 | 8 +- .../CIS/Identity/Invoke-CippTestCIS_1_3_1.ps1 | 6 +- .../Identity/Invoke-CippTestCIS_2_1_10.ps1 | 6 +- .../Identity/Invoke-CippTestCIS_2_1_14.ps1 | 6 +- .../CIS/Identity/Invoke-CippTestCIS_2_1_8.ps1 | 6 +- .../CIS/Identity/Invoke-CippTestCIS_2_1_9.ps1 | 6 +- .../Identity/Invoke-CippTestCIS_5_2_2_1.ps1 | 8 +- .../Identity/Invoke-CippTestCIS_5_2_2_4.ps1 | 8 +- .../CIS/Identity/Invoke-CippTestCIS_5_3_1.ps1 | 4 +- .../Identity/Invoke-CippTestCISAMSEXO101.ps1 | 10 +- .../Identity/Invoke-CippTestCISAMSEXO102.ps1 | 10 +- .../Identity/Invoke-CippTestCISAMSEXO103.ps1 | 10 +- .../Identity/Invoke-CippTestCISAMSEXO11.ps1 | 10 +- .../Identity/Invoke-CippTestCISAMSEXO112.ps1 | 12 +-- .../Identity/Invoke-CippTestCISAMSEXO113.ps1 | 12 +-- .../Identity/Invoke-CippTestCISAMSEXO121.ps1 | 14 +-- .../Identity/Invoke-CippTestCISAMSEXO122.ps1 | 10 +- .../Identity/Invoke-CippTestCISAMSEXO131.ps1 | 8 +- .../Identity/Invoke-CippTestCISAMSEXO141.ps1 | 10 +- .../Identity/Invoke-CippTestCISAMSEXO142.ps1 | 10 +- .../Identity/Invoke-CippTestCISAMSEXO143.ps1 | 10 +- .../Identity/Invoke-CippTestCISAMSEXO151.ps1 | 10 +- .../Identity/Invoke-CippTestCISAMSEXO152.ps1 | 10 +- .../Identity/Invoke-CippTestCISAMSEXO153.ps1 | 10 +- .../Identity/Invoke-CippTestCISAMSEXO171.ps1 | 12 +-- .../Identity/Invoke-CippTestCISAMSEXO173.ps1 | 12 +-- .../Identity/Invoke-CippTestCISAMSEXO31.ps1 | 10 +- .../Identity/Invoke-CippTestCISAMSEXO51.ps1 | 14 +-- .../Identity/Invoke-CippTestCISAMSEXO61.ps1 | 10 +- .../Identity/Invoke-CippTestCISAMSEXO62.ps1 | 10 +- .../Identity/Invoke-CippTestCISAMSEXO71.ps1 | 8 +- .../Identity/Invoke-CippTestCISAMSEXO95.ps1 | 10 +- .../Invoke-CippTestCopilotReady001.ps1 | 14 +-- .../Invoke-CippTestCopilotReady002.ps1 | 22 ++--- .../Invoke-CippTestCopilotReady003.ps1 | 20 ++-- .../Invoke-CippTestCopilotReady004.ps1 | 16 ++-- .../Invoke-CippTestCopilotReady005.ps1 | 16 ++-- .../Invoke-CippTestCopilotReady006.ps1 | 16 ++-- .../Invoke-CippTestCopilotReady007.ps1 | 18 ++-- .../Invoke-CippTestCopilotReady008.ps1 | 22 ++--- .../Invoke-CippTestCopilotReady009.ps1 | 10 +- .../Invoke-CippTestCopilotReady010.ps1 | 14 +-- .../Invoke-CippTestCopilotReady011.ps1 | 20 ++-- .../Invoke-CippTestCopilotReady012.ps1 | 18 ++-- .../Invoke-CippTestCopilotReady013.ps1 | 16 ++-- .../Invoke-CippTestCopilotReady014.ps1 | 16 ++-- .../Invoke-CippTestCopilotReady015.ps1 | 4 +- .../Invoke-CippTestCopilotReady016.ps1 | 14 +-- .../Invoke-CippTestCopilotReady017.ps1 | 14 +-- .../Custom/Invoke-CippTestCustomScripts.ps1 | 36 +++++--- .../Invoke-CippTestGenericTest001.ps1 | 8 +- .../Invoke-CippTestGenericTest002.ps1 | 12 +-- .../Invoke-CippTestGenericTest003.ps1 | 14 +-- .../Invoke-CippTestGenericTest004.ps1 | 34 +++---- .../Invoke-CippTestGenericTest005.ps1 | 18 ++-- .../Invoke-CippTestGenericTest006.ps1 | 18 ++-- .../Invoke-CippTestGenericTest007.ps1 | 18 ++-- .../Invoke-CippTestGenericTest008.ps1 | 42 ++++----- .../Invoke-CippTestGenericTest009.ps1 | 34 +++---- .../Invoke-CippTestGenericTest010.ps1 | 22 ++--- .../Invoke-CippTestGenericTest011.ps1 | 54 +++++------ .../ORCA/Identity/Invoke-CippTestORCA100.ps1 | 14 +-- .../ORCA/Identity/Invoke-CippTestORCA101.ps1 | 20 ++-- .../ORCA/Identity/Invoke-CippTestORCA102.ps1 | 14 +-- .../ORCA/Identity/Invoke-CippTestORCA103.ps1 | 14 +-- .../ORCA/Identity/Invoke-CippTestORCA104.ps1 | 24 ++--- .../ORCA/Identity/Invoke-CippTestORCA105.ps1 | 14 +-- .../ORCA/Identity/Invoke-CippTestORCA106.ps1 | 14 +-- .../ORCA/Identity/Invoke-CippTestORCA107.ps1 | 22 ++--- .../Identity/Invoke-CippTestORCA108_1.ps1 | 14 +-- .../ORCA/Identity/Invoke-CippTestORCA109.ps1 | 14 +-- .../ORCA/Identity/Invoke-CippTestORCA110.ps1 | 14 +-- .../ORCA/Identity/Invoke-CippTestORCA111.ps1 | 14 +-- .../ORCA/Identity/Invoke-CippTestORCA112.ps1 | 14 +-- .../ORCA/Identity/Invoke-CippTestORCA113.ps1 | 24 ++--- .../ORCA/Identity/Invoke-CippTestORCA114.ps1 | 14 +-- .../ORCA/Identity/Invoke-CippTestORCA115.ps1 | 14 +-- .../ORCA/Identity/Invoke-CippTestORCA116.ps1 | 14 +-- .../Identity/Invoke-CippTestORCA118_1.ps1 | 14 +-- .../Identity/Invoke-CippTestORCA118_2.ps1 | 14 +-- .../Identity/Invoke-CippTestORCA118_3.ps1 | 14 +-- .../Identity/Invoke-CippTestORCA118_4.ps1 | 14 +-- .../ORCA/Identity/Invoke-CippTestORCA119.ps1 | 14 +-- .../Invoke-CippTestORCA120_malware.ps1 | 14 +-- .../Identity/Invoke-CippTestORCA120_phish.ps1 | 14 +-- .../Identity/Invoke-CippTestORCA120_spam.ps1 | 14 +-- .../ORCA/Identity/Invoke-CippTestORCA121.ps1 | 4 +- .../ORCA/Identity/Invoke-CippTestORCA123.ps1 | 14 +-- .../ORCA/Identity/Invoke-CippTestORCA124.ps1 | 14 +-- .../ORCA/Identity/Invoke-CippTestORCA139.ps1 | 14 +-- .../ORCA/Identity/Invoke-CippTestORCA140.ps1 | 14 +-- .../ORCA/Identity/Invoke-CippTestORCA141.ps1 | 14 +-- .../ORCA/Identity/Invoke-CippTestORCA142.ps1 | 14 +-- .../ORCA/Identity/Invoke-CippTestORCA143.ps1 | 14 +-- .../ORCA/Identity/Invoke-CippTestORCA156.ps1 | 14 +-- .../ORCA/Identity/Invoke-CippTestORCA158.ps1 | 8 +- .../ORCA/Identity/Invoke-CippTestORCA179.ps1 | 14 +-- .../ORCA/Identity/Invoke-CippTestORCA180.ps1 | 14 +-- .../ORCA/Identity/Invoke-CippTestORCA189.ps1 | 10 +- .../Identity/Invoke-CippTestORCA189_2.ps1 | 10 +- .../ORCA/Identity/Invoke-CippTestORCA205.ps1 | 14 +-- .../ORCA/Identity/Invoke-CippTestORCA220.ps1 | 14 +-- .../ORCA/Identity/Invoke-CippTestORCA221.ps1 | 14 +-- .../ORCA/Identity/Invoke-CippTestORCA222.ps1 | 14 +-- .../ORCA/Identity/Invoke-CippTestORCA223.ps1 | 14 +-- .../ORCA/Identity/Invoke-CippTestORCA224.ps1 | 14 +-- .../ORCA/Identity/Invoke-CippTestORCA225.ps1 | 8 +- .../ORCA/Identity/Invoke-CippTestORCA226.ps1 | 12 +-- .../ORCA/Identity/Invoke-CippTestORCA227.ps1 | 12 +-- .../ORCA/Identity/Invoke-CippTestORCA228.ps1 | 14 +-- .../ORCA/Identity/Invoke-CippTestORCA229.ps1 | 14 +-- .../ORCA/Identity/Invoke-CippTestORCA230.ps1 | 12 +-- .../ORCA/Identity/Invoke-CippTestORCA231.ps1 | 12 +-- .../ORCA/Identity/Invoke-CippTestORCA232.ps1 | 12 +-- .../ORCA/Identity/Invoke-CippTestORCA233.ps1 | 10 +- .../Identity/Invoke-CippTestORCA233_1.ps1 | 8 +- .../ORCA/Identity/Invoke-CippTestORCA234.ps1 | 8 +- .../ORCA/Identity/Invoke-CippTestORCA235.ps1 | 14 +-- .../ORCA/Identity/Invoke-CippTestORCA236.ps1 | 14 +-- .../ORCA/Identity/Invoke-CippTestORCA237.ps1 | 14 +-- .../ORCA/Identity/Invoke-CippTestORCA238.ps1 | 14 +-- .../ORCA/Identity/Invoke-CippTestORCA239.ps1 | 8 +- .../ORCA/Identity/Invoke-CippTestORCA240.ps1 | 8 +- .../ORCA/Identity/Invoke-CippTestORCA241.ps1 | 14 +-- .../ORCA/Identity/Invoke-CippTestORCA242.ps1 | 18 ++-- .../ORCA/Identity/Invoke-CippTestORCA243.ps1 | 12 +-- .../ORCA/Identity/Invoke-CippTestORCA244.ps1 | 14 +-- .../ZTNA/Devices/Invoke-CippTestZTNA24541.ps1 | 12 +-- .../ZTNA/Devices/Invoke-CippTestZTNA24542.ps1 | 12 +-- .../ZTNA/Devices/Invoke-CippTestZTNA24543.ps1 | 12 +-- .../ZTNA/Devices/Invoke-CippTestZTNA24545.ps1 | 12 +-- .../ZTNA/Devices/Invoke-CippTestZTNA24547.ps1 | 12 +-- .../ZTNA/Devices/Invoke-CippTestZTNA24548.ps1 | 12 +-- .../ZTNA/Devices/Invoke-CippTestZTNA24549.ps1 | 12 +-- .../ZTNA/Devices/Invoke-CippTestZTNA24553.ps1 | 12 +-- .../ZTNA/Devices/Invoke-CippTestZTNA24569.ps1 | 14 +-- .../ZTNA/Devices/Invoke-CippTestZTNA24576.ps1 | 14 +-- .../ZTNA/Devices/Invoke-CippTestZTNA24839.ps1 | 14 +-- .../ZTNA/Devices/Invoke-CippTestZTNA24840.ps1 | 14 +-- .../ZTNA/Devices/Invoke-CippTestZTNA24870.ps1 | 14 +-- .../Identity/Invoke-CippTestZTNA21797.ps1 | 42 ++++----- .../Identity/Invoke-CippTestZTNA21799.ps1 | 8 +- .../Identity/Invoke-CippTestZTNA21804.ps1 | 8 +- .../Identity/Invoke-CippTestZTNA21806.ps1 | 10 +- .../Identity/Invoke-CippTestZTNA21811.ps1 | 16 ++-- .../Identity/Invoke-CippTestZTNA21812.ps1 | 14 +-- .../Identity/Invoke-CippTestZTNA21813.ps1 | 16 ++-- .../Identity/Invoke-CippTestZTNA21814.ps1 | 12 +-- .../Identity/Invoke-CippTestZTNA21815.ps1 | 12 +-- .../Identity/Invoke-CippTestZTNA21816.ps1 | 87 ++++++++++-------- .../Identity/Invoke-CippTestZTNA21818.ps1 | 12 +-- .../Identity/Invoke-CippTestZTNA21819.ps1 | 10 +- .../Identity/Invoke-CippTestZTNA21820.ps1 | 14 +-- .../Identity/Invoke-CippTestZTNA21822.ps1 | 22 ++--- .../Identity/Invoke-CippTestZTNA21824.ps1 | 8 +- .../Identity/Invoke-CippTestZTNA21825.ps1 | 32 +++---- .../Identity/Invoke-CippTestZTNA21828.ps1 | 8 +- .../Identity/Invoke-CippTestZTNA21829.ps1 | 12 +-- .../Identity/Invoke-CippTestZTNA21830.ps1 | 12 +-- .../Identity/Invoke-CippTestZTNA21835.ps1 | 38 ++++---- .../Identity/Invoke-CippTestZTNA21836.ps1 | 16 ++-- .../Identity/Invoke-CippTestZTNA21838.ps1 | 14 +-- .../Identity/Invoke-CippTestZTNA21839.ps1 | 26 +++--- .../Identity/Invoke-CippTestZTNA21840.ps1 | 22 ++--- .../Identity/Invoke-CippTestZTNA21845.ps1 | 16 ++-- .../Identity/Invoke-CippTestZTNA21846.ps1 | 12 +-- .../Identity/Invoke-CippTestZTNA21848.ps1 | 12 +-- .../Identity/Invoke-CippTestZTNA21849.ps1 | 32 +++---- .../Identity/Invoke-CippTestZTNA21850.ps1 | 16 ++-- .../Identity/Invoke-CippTestZTNA21861.ps1 | 12 +-- .../Identity/Invoke-CippTestZTNA21862.ps1 | 20 ++-- .../Identity/Invoke-CippTestZTNA21863.ps1 | 12 +-- .../Identity/Invoke-CippTestZTNA21865.ps1 | 14 +-- .../Identity/Invoke-CippTestZTNA21866.ps1 | 12 +-- .../Identity/Invoke-CippTestZTNA21872.ps1 | 24 ++--- .../Identity/Invoke-CippTestZTNA21883.ps1 | 14 +-- .../Identity/Invoke-CippTestZTNA21889.ps1 | 14 +-- .../Identity/Invoke-CippTestZTNA21892.ps1 | 14 +-- .../Identity/Invoke-CippTestZTNA21941.ps1 | 24 ++--- .../Identity/Invoke-CippTestZTNA21953.ps1 | 8 +- .../Identity/Invoke-CippTestZTNA21954.ps1 | 8 +- .../Identity/Invoke-CippTestZTNA21955.ps1 | 8 +- .../Identity/Invoke-CippTestZTNA21964.ps1 | 12 +-- .../Identity/Invoke-CippTestZTNA22124.ps1 | 16 ++-- .../Identity/Invoke-CippTestZTNA22659.ps1 | 16 ++-- .../Identity/Invoke-CippTestZTNA24570.ps1 | 20 ++-- .../Identity/Invoke-CippTestZTNA24572.ps1 | 12 +-- .../Identity/Invoke-CippTestZTNA24824.ps1 | 16 ++-- .../Identity/Invoke-CippTestZTNA24827.ps1 | 16 ++-- Shared/CIPPSharp/CIPPTestDataCache.cs | 39 +++++--- Shared/CIPPSharp/bin/CIPPSharp.dll | Bin 41984 -> 41472 bytes 207 files changed, 1645 insertions(+), 1491 deletions(-) diff --git a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/CIPPDBCache/Push-ExecCIPPDBCache.ps1 b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/CIPPDBCache/Push-ExecCIPPDBCache.ps1 index c9a16df871ff..522d40cecc12 100644 --- a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/CIPPDBCache/Push-ExecCIPPDBCache.ps1 +++ b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/CIPPDBCache/Push-ExecCIPPDBCache.ps1 @@ -45,8 +45,19 @@ function Push-ExecCIPPDBCache { # Build the full function name $FullFunctionName = "Set-CIPPDBCache$Name" - # Check if function exists - $Function = Get-Command -Name $FullFunctionName -ErrorAction SilentlyContinue + # Cache the resolved command per process so back-to-back HTTP-driven refreshes + # don't repeat the module command-table walk. + if (-not $script:CIPPDBCacheFunctionLookup) { + $script:CIPPDBCacheFunctionLookup = [System.Collections.Generic.Dictionary[string, object]]::new([System.StringComparer]::OrdinalIgnoreCase) + Write-Information "[CacheInit] CIPPDBCacheFunctionLookup initialized in PID $PID" + } + if ($script:CIPPDBCacheFunctionLookup.ContainsKey($FullFunctionName)) { + Write-Information "[CacheHit] CIPPDBCacheFunctionLookup PID=$PID Key=$FullFunctionName Size=$($script:CIPPDBCacheFunctionLookup.Count)" + } else { + Write-Information "[CacheMiss] CIPPDBCacheFunctionLookup PID=$PID Key=$FullFunctionName Size=$($script:CIPPDBCacheFunctionLookup.Count) - resolving via Get-Command" + $script:CIPPDBCacheFunctionLookup[$FullFunctionName] = Get-Command -Name $FullFunctionName -ErrorAction SilentlyContinue + } + $Function = $script:CIPPDBCacheFunctionLookup[$FullFunctionName] if (-not $Function) { throw "Function $FullFunctionName does not exist" } diff --git a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Tests/Push-CIPPTest.ps1 b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Tests/Push-CIPPTest.ps1 index 07574ef2b225..302276834eb2 100644 --- a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Tests/Push-CIPPTest.ps1 +++ b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Tests/Push-CIPPTest.ps1 @@ -12,6 +12,14 @@ function Push-CIPPTest { Write-Information "Running test $TestId for tenant $TenantFilter" + # Per-process cache of resolved test function commands so that a flat orchestrator + # firing thousands of activities doesn't repeat the module command-table walk + # for every task. + if (-not $script:CIPPTestFunctionLookup) { + $script:CIPPTestFunctionLookup = [System.Collections.Generic.Dictionary[string, object]]::new([System.StringComparer]::OrdinalIgnoreCase) + Write-Information "[CacheInit] CIPPTestFunctionLookup initialized in PID $PID" + } + try { if ($TestId -like 'CustomScript-*') { $ScriptGuid = $TestId -replace '^CustomScript-', '' @@ -23,13 +31,20 @@ function Push-CIPPTest { $FunctionName = "Invoke-CippTest$TestId" - if (-not (Get-Command $FunctionName -Module CIPPTests -ErrorAction SilentlyContinue)) { + if ($script:CIPPTestFunctionLookup.ContainsKey($FunctionName)) { + Write-Information "[CacheHit] CIPPTestFunctionLookup PID=$PID Key=$FunctionName Size=$($script:CIPPTestFunctionLookup.Count)" + } else { + Write-Information "[CacheMiss] CIPPTestFunctionLookup PID=$PID Key=$FunctionName Size=$($script:CIPPTestFunctionLookup.Count) - resolving via Get-Command" + $script:CIPPTestFunctionLookup[$FunctionName] = Get-Command $FunctionName -Module CIPPTests -ErrorAction SilentlyContinue + } + $TestCommand = $script:CIPPTestFunctionLookup[$FunctionName] + if (-not $TestCommand) { Write-LogMessage -API 'Tests' -tenant $TenantFilter -message "Test function not found: $FunctionName" -sev Error return @{ testRun = $false } } Write-Information "Executing $FunctionName for $TenantFilter" - & $FunctionName -Tenant $TenantFilter + & $TestCommand -Tenant $TenantFilter Write-Host "Returning true, test has run for $tenantFilter" return @{ testRun = $true } diff --git a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Tests/Push-CIPPTestsList.ps1 b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Tests/Push-CIPPTestsList.ps1 index 916fc512fba9..d018d1d0aa9d 100644 --- a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Tests/Push-CIPPTestsList.ps1 +++ b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Tests/Push-CIPPTestsList.ps1 @@ -22,11 +22,17 @@ function Push-CIPPTestsList { try { Write-Information "Building test suite list for tenant: $TenantFilter" - # Check if tenant has data before emitting any work - $DbCounts = Get-CIPPDbItem -TenantFilter $TenantFilter -CountsOnly - if (($DbCounts | Measure-Object -Property DataCount -Sum).Sum -eq 0) { - Write-Information "Tenant $TenantFilter has no data in database. Skipping tests." - return @() + # The orchestrator (Start-CIPPDBTestsRun) already filtered the tenant list to those + # with cached data, so the previous per-tenant `Get-CIPPDbItem -CountsOnly` recheck + # was a redundant Table query (one extra round-trip per tenant). The orchestrator + # may pass SkipDbCheck=$true when it has already verified data presence; otherwise + # we fall back to a check here for any direct invocations. + if (-not $Item.SkipDbCheck) { + $DbCounts = Get-CIPPDbItem -TenantFilter $TenantFilter -CountsOnly + if (($DbCounts | Measure-Object -Property DataCount -Sum).Sum -eq 0) { + Write-Information "Tenant $TenantFilter has no data in database. Skipping tests." + return @() + } } # Emit one task per suite — suite names must match the ValidateSet in Invoke-CIPPTestCollection. diff --git a/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 b/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 index ba74ec1096d8..d0c5e5ad27f1 100644 --- a/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 @@ -29,10 +29,15 @@ function Add-CIPPDbItem { $Batch = [System.Collections.Generic.List[hashtable]]::new() $NewRowKeys = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) $TotalProcessed = 0 + # Cache regex instances so each row pays only the match cost, not regex compilation. + # Two passes preserve the original semantics: path/wildcard chars → '_', control chars → stripped. + $RowKeyPathRegex = [regex]::new('[/\\#?]') + $RowKeyControlRegex = [regex]::new('[\u0000-\u001F\u007F-\u009F]') if ($TenantFilter -match '^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$') { try { - $TenantFilter = (Get-Tenants -TenantFilter $TenantFilter -IncludeErrors | Select-Object -First 1).defaultDomainName + $TenantLookup = @(Get-Tenants -TenantFilter $TenantFilter -IncludeErrors) + if ($TenantLookup.Count -gt 0) { $TenantFilter = $TenantLookup[0].defaultDomainName } } catch {} } } @@ -48,7 +53,7 @@ function Add-CIPPDbItem { foreach ($Item in @($InputObject)) { if ($null -eq $Item) { continue } $ItemId = $Item.ExternalDirectoryObjectId ?? $Item.id ?? $Item.Identity ?? $Item.skuId ?? $Item.userPrincipalName ?? [guid]::NewGuid().ToString() - $RowKey = "$Type-$ItemId" -replace '[/\\#?]', '_' -replace '[\u0000-\u001F\u007F-\u009F]', '' + $RowKey = $RowKeyControlRegex.Replace($RowKeyPathRegex.Replace("$Type-$ItemId", '_'), '') if ($NewRowKeys.Add($RowKey)) { $Batch.Add(@{ PartitionKey = $TenantFilter diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPDBTestsRun.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPDBTestsRun.ps1 index f310233164ba..00585cdbf107 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPDBTestsRun.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPDBTestsRun.ps1 @@ -58,11 +58,14 @@ function Start-CIPPDBTestsRun { return } - # Phase 1: Build per-tenant list activities (discover tests per tenant) + # Phase 1: Build per-tenant list activities (discover tests per tenant). + # The tenants below were already filtered by data presence above, so we pass + # SkipDbCheck=$true to avoid a redundant CountsOnly round-trip per tenant. $Batch = foreach ($Tenant in $AllTenantsList) { @{ FunctionName = 'CIPPTestsList' TenantFilter = $Tenant + SkipDbCheck = $true } } diff --git a/Modules/CIPPCore/Public/Get-CIPPDbItem.ps1 b/Modules/CIPPCore/Public/Get-CIPPDbItem.ps1 index 0fcb380e5aa1..ba4f2d4a5d03 100644 --- a/Modules/CIPPCore/Public/Get-CIPPDbItem.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPDbItem.ps1 @@ -62,8 +62,9 @@ function Get-CIPPDbItem { $Conditions.Add('DataCount ge 0') } $Filter = [string]::Join(' and ', $Conditions) + # -Property does the projection server-side; the trailing Select-Object was + # redundant (and rebuilt every row as a NoteProperty bag, slowing later filters). $Results = Get-CIPPAzDataTableEntity @Table -Filter $Filter -Property 'PartitionKey', 'RowKey', 'DataCount', 'Timestamp' - $Results = $Results | Select-Object PartitionKey, RowKey, DataCount, Timestamp } else { if (-not $Type) { throw 'Type parameter is required when CountsOnly is not specified' diff --git a/Modules/CIPPCore/Public/Get-CIPPDomainAnalyser.ps1 b/Modules/CIPPCore/Public/Get-CIPPDomainAnalyser.ps1 index e1aed77dbd55..f2494efa09db 100644 --- a/Modules/CIPPCore/Public/Get-CIPPDomainAnalyser.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPDomainAnalyser.ps1 @@ -14,6 +14,17 @@ function Get-CIPPDomainAnalyser { #> [CmdletBinding()] param([string]$TenantFilter) + + if (-not $script:CIPPDomainAnalyserCache) { + $script:CIPPDomainAnalyserCache = @{} + } + $CacheKey = if ([string]::IsNullOrEmpty($TenantFilter)) { 'AllTenants' } else { $TenantFilter } + $CacheNow = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() + $CachedEntry = $script:CIPPDomainAnalyserCache[$CacheKey] + if ($CachedEntry -and ($CacheNow - $CachedEntry.Timestamp) -lt 300) { + return $CachedEntry.Results + } + $DomainTable = Get-CIPPTable -Table 'Domains' # Get all the things @@ -43,5 +54,6 @@ function Get-CIPPDomainAnalyser { } catch { $Results = @() } + $script:CIPPDomainAnalyserCache[$CacheKey] = @{ Results = $Results; Timestamp = $CacheNow } return $Results } diff --git a/Modules/CIPPCore/Public/Get-CippDbRole.ps1 b/Modules/CIPPCore/Public/Get-CippDbRole.ps1 index 02313a9a96ad..5c38f0eb63a4 100644 --- a/Modules/CIPPCore/Public/Get-CippDbRole.ps1 +++ b/Modules/CIPPCore/Public/Get-CippDbRole.ps1 @@ -11,7 +11,7 @@ function Get-CippDbRole { [switch]$CisaHighlyPrivilegedRoles ) - $Roles = New-CIPPDbRequest -TenantFilter $TenantFilter -Type 'Roles' + $Roles = Get-CIPPTestData -TenantFilter $TenantFilter -Type 'Roles' if ($IncludePrivilegedRoles) { $PrivilegedRoleTemplateIds = @( diff --git a/Modules/CIPPCore/Public/Get-CippDbRoleMembers.ps1 b/Modules/CIPPCore/Public/Get-CippDbRoleMembers.ps1 index d51612448da5..0b5b026df61a 100644 --- a/Modules/CIPPCore/Public/Get-CippDbRoleMembers.ps1 +++ b/Modules/CIPPCore/Public/Get-CippDbRoleMembers.ps1 @@ -8,9 +8,9 @@ function Get-CippDbRoleMembers { [string]$RoleTemplateId ) - $RoleAssignments = New-CIPPDbRequest -TenantFilter $TenantFilter -Type 'RoleAssignmentScheduleInstances' - $RoleEligibilities = New-CIPPDbRequest -TenantFilter $TenantFilter -Type 'RoleEligibilitySchedules' - $DirectRoleAssignments = New-CIPPDbRequest -TenantFilter $TenantFilter -Type 'Roles' | Where-Object { $_.roleTemplateId -eq $RoleTemplateId } | Select-Object -ExpandProperty members + $RoleAssignments = Get-CIPPTestData -TenantFilter $TenantFilter -Type 'RoleAssignmentScheduleInstances' + $RoleEligibilities = Get-CIPPTestData -TenantFilter $TenantFilter -Type 'RoleEligibilitySchedules' + $DirectRoleAssignments = Get-CIPPTestData -TenantFilter $TenantFilter -Type 'Roles' | Where-Object { $_.roleTemplateId -eq $RoleTemplateId } | Select-Object -ExpandProperty members $ActiveMembers = $RoleAssignments | Where-Object { $_.roleDefinitionId -eq $RoleTemplateId -and $_.assignmentType -eq 'Assigned' diff --git a/Modules/CIPPCore/Public/Invoke-CIPPDBCacheCollection.ps1 b/Modules/CIPPCore/Public/Invoke-CIPPDBCacheCollection.ps1 index c4af3e75d077..2538fc8493fb 100644 --- a/Modules/CIPPCore/Public/Invoke-CIPPDBCacheCollection.ps1 +++ b/Modules/CIPPCore/Public/Invoke-CIPPDBCacheCollection.ps1 @@ -181,13 +181,13 @@ function Invoke-CIPPDBCacheCollection { Write-Information " [$CollectionType] Collecting $CacheType for $TenantFilter" & $FullFunctionName @Params $ItemStopwatch.Stop() - $ElapsedSeconds = [math]::Round($ItemStopwatch.Elapsed.TotalSeconds, 3) + $ElapsedSeconds = '{0:N3}' -f $ItemStopwatch.Elapsed.TotalSeconds $Timings.Add("$CacheType : ${ElapsedSeconds}s") Write-Information " [$CollectionType] Completed $CacheType for $TenantFilter - Took ${ElapsedSeconds} seconds" $SuccessCount++ } catch { $ItemStopwatch.Stop() - $ElapsedSeconds = [math]::Round($ItemStopwatch.Elapsed.TotalSeconds, 3) + $ElapsedSeconds = '{0:N3}' -f $ItemStopwatch.Elapsed.TotalSeconds $FailedCount++ $Errors.Add("$CacheType : $($_.Exception.Message)") $Timings.Add("$CacheType : ${ElapsedSeconds}s (FAILED)") @@ -196,7 +196,7 @@ function Invoke-CIPPDBCacheCollection { } $CollectionStopwatch.Stop() - $TotalElapsed = [math]::Round($CollectionStopwatch.Elapsed.TotalSeconds, 3) + $TotalElapsed = '{0:N3}' -f $CollectionStopwatch.Elapsed.TotalSeconds $Summary = "$CollectionType collection for $TenantFilter completed in ${TotalElapsed} seconds - $SuccessCount succeeded, $FailedCount failed out of $($CacheTypes.Count)" Write-Information $Summary Write-Information " Timings: $($Timings -join ' | ')" diff --git a/Modules/CIPPCore/Public/Invoke-CIPPTestCollection.ps1 b/Modules/CIPPCore/Public/Invoke-CIPPTestCollection.ps1 index b386e1e4413b..052242225f8d 100644 --- a/Modules/CIPPCore/Public/Invoke-CIPPTestCollection.ps1 +++ b/Modules/CIPPCore/Public/Invoke-CIPPTestCollection.ps1 @@ -53,6 +53,21 @@ function Invoke-CIPPTestCollection { GenericTests = 'Invoke-CippTestGenericTest*' } + # Process-scoped cache of Get-Command lookups so each (tenant × suite) invocation + # doesn't pay the module command-table walk again. The CIPPTests module is loaded + # once per worker process and its exported function set does not change at runtime. + if (-not $script:CIPPTestSuiteFunctionCache) { + $script:CIPPTestSuiteFunctionCache = [System.Collections.Generic.Dictionary[string, object]]::new([System.StringComparer]::OrdinalIgnoreCase) + Write-Information "[CacheInit] CIPPTestSuiteFunctionCache initialized in PID $PID" + } + if (-not $script:CIPPTestCustomFunctionResolved) { + Write-Information "[CacheMiss] CIPPTestCustomFunction PID=$PID - resolving Invoke-CippTestCustomScripts via Get-Command" + $script:CIPPTestCustomFunction = Get-Command -Name 'Invoke-CippTestCustomScripts' -ErrorAction SilentlyContinue + $script:CIPPTestCustomFunctionResolved = $true + } else { + Write-Information "[CacheHit] CIPPTestCustomFunction PID=$PID Resolved=$([bool]$script:CIPPTestCustomFunction)" + } + $SuiteStopwatch = [System.Diagnostics.Stopwatch]::StartNew() $SuccessCount = 0 $FailedCount = 0 @@ -62,8 +77,7 @@ function Invoke-CIPPTestCollection { # Custom suite: Invoke-CippTestCustomScripts now requires a ScriptGuid parameter. # Enumerate distinct enabled script guids from the DB and call once per guid. if ($SuiteName -eq 'Custom') { - $CustomFunction = Get-Command -Name 'Invoke-CippTestCustomScripts' -ErrorAction SilentlyContinue - if (-not $CustomFunction) { + if (-not $script:CIPPTestCustomFunction) { Write-Information 'Invoke-CippTestCustomScripts not found — skipping Custom suite' return @{ SuiteName = $SuiteName; TenantFilter = $TenantFilter; Success = 0; Failed = 0; Total = 0; TotalSeconds = 0; Timings = @(); Errors = @() } } @@ -71,12 +85,30 @@ function Invoke-CIPPTestCollection { $Table = Get-CippTable -TableName 'CustomPowershellScripts' $AllScripts = @(Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq 'CustomScript'") - # Get the latest version of each script guid, filter to enabled only - $EnabledGuids = $AllScripts | Group-Object -Property ScriptGuid | ForEach-Object { - $_.Group | Sort-Object -Property Version -Descending | Select-Object -First 1 - } | Where-Object { - -not $_.PSObject.Properties['Enabled'] -or [bool]$_.Enabled - } | Select-Object -ExpandProperty ScriptGuid + # Single-pass "latest enabled version per ScriptGuid". + # The previous Group-Object | ForEach-Object { Sort-Object | Select -First 1 } + # pipeline allocated a Group container per guid and ran an O(n log n) sort per group; + # this hashtable walk is O(n) total and avoids the pipeline overhead entirely. + $LatestByGuid = @{} + foreach ($Script in $AllScripts) { + $Guid = $Script.ScriptGuid + if (-not $Guid) { continue } + $Existing = $LatestByGuid[$Guid] + if (-not $Existing -or [int]$Script.Version -gt [int]$Existing.Version) { + $LatestByGuid[$Guid] = $Script + } + } + + $EnabledGuidsList = [System.Collections.Generic.List[string]]::new() + foreach ($Latest in $LatestByGuid.Values) { + # Cache the property lookup — calling .PSObject.Properties[''] reflects through + # the PSObject member set on every invocation in the original code. + $EnabledProp = $Latest.PSObject.Properties['Enabled'] + if (-not $EnabledProp -or [bool]$EnabledProp.Value) { + $EnabledGuidsList.Add($Latest.ScriptGuid) + } + } + $EnabledGuids = $EnabledGuidsList.ToArray() if ($EnabledGuids.Count -eq 0) { Write-Information 'No enabled custom scripts found — skipping Custom suite' @@ -104,13 +136,13 @@ function Invoke-CIPPTestCollection { $ResultBatch.Clear() } $ItemStopwatch.Stop() - $ElapsedSeconds = [math]::Round($ItemStopwatch.Elapsed.TotalSeconds, 3) + $ElapsedSeconds = '{0:N3}' -f $ItemStopwatch.Elapsed.TotalSeconds $Timings.Add("CustomScript-$Guid : ${ElapsedSeconds}s") Write-Information " [Custom] Completed CustomScript-$Guid - ${ElapsedSeconds}s" $SuccessCount++ } catch { $ItemStopwatch.Stop() - $ElapsedSeconds = [math]::Round($ItemStopwatch.Elapsed.TotalSeconds, 3) + $ElapsedSeconds = '{0:N3}' -f $ItemStopwatch.Elapsed.TotalSeconds $FailedCount++ $Errors.Add("CustomScript-$Guid : $($_.Exception.Message)") $Timings.Add("CustomScript-$Guid : ${ElapsedSeconds}s (FAILED)") @@ -125,7 +157,7 @@ function Invoke-CIPPTestCollection { } $SuiteStopwatch.Stop() - $TotalElapsed = [math]::Round($SuiteStopwatch.Elapsed.TotalSeconds, 3) + $TotalElapsed = '{0:N3}' -f $SuiteStopwatch.Elapsed.TotalSeconds $Summary = "Custom suite for $TenantFilter completed in ${TotalElapsed}s — $SuccessCount/$($EnabledGuids.Count) ran, $FailedCount errored" Write-Information $Summary Write-Information " Timings: $($Timings -join ' | ')" @@ -144,9 +176,17 @@ function Invoke-CIPPTestCollection { } } - # Standard suites: discover functions by name pattern via Get-Command + # Standard suites: discover functions by name pattern via Get-Command. + # Cache the function list per suite so repeated activity invocations in the same + # process don't pay the module command-table walk again (item 7). $Pattern = $SuitePatterns[$SuiteName] - $TestFunctions = @(Get-Command -Name $Pattern -Module CIPPTests -ErrorAction SilentlyContinue) + if ($script:CIPPTestSuiteFunctionCache.ContainsKey($SuiteName)) { + Write-Information "[CacheHit] CIPPTestSuiteFunctionCache PID=$PID Suite=$SuiteName Size=$($script:CIPPTestSuiteFunctionCache.Count)" + } else { + Write-Information "[CacheMiss] CIPPTestSuiteFunctionCache PID=$PID Suite=$SuiteName Size=$($script:CIPPTestSuiteFunctionCache.Count) - resolving pattern '$Pattern' via Get-Command" + $script:CIPPTestSuiteFunctionCache[$SuiteName] = @(Get-Command -Name $Pattern -Module CIPPTests -ErrorAction SilentlyContinue) + } + $TestFunctions = $script:CIPPTestSuiteFunctionCache[$SuiteName] if ($TestFunctions.Count -eq 0) { Write-Information "No test functions found for suite $SuiteName (pattern: $Pattern) — skipping" return @{ @@ -180,13 +220,13 @@ function Invoke-CIPPTestCollection { $ResultBatch.Clear() } $ItemStopwatch.Stop() - $Timings.Add("$($TestFunction.Name) : $([math]::Round($ItemStopwatch.Elapsed.TotalSeconds, 3))s") + $Timings.Add(('{0} : {1:N3}s' -f $TestFunction.Name, $ItemStopwatch.Elapsed.TotalSeconds)) $SuccessCount++ } catch { $ItemStopwatch.Stop() $FailedCount++ $Errors.Add("$($TestFunction.Name) : $($_.Exception.Message)") - $Timings.Add("$($TestFunction.Name) : $([math]::Round($ItemStopwatch.Elapsed.TotalSeconds, 3))s (FAILED)") + $Timings.Add(('{0} : {1:N3}s (FAILED)' -f $TestFunction.Name, $ItemStopwatch.Elapsed.TotalSeconds)) } } @@ -197,7 +237,7 @@ function Invoke-CIPPTestCollection { } $SuiteStopwatch.Stop() - $TotalElapsed = [math]::Round($SuiteStopwatch.Elapsed.TotalSeconds, 3) + $TotalElapsed = '{0:N3}' -f $SuiteStopwatch.Elapsed.TotalSeconds $TestCount = $TestFunctions.Count $Summary = "$SuiteName suite for $TenantFilter completed in ${TotalElapsed}s — $SuccessCount/$TestCount ran, $FailedCount errored" Write-Information $Summary diff --git a/Modules/CIPPCore/Public/New-CIPPDbRequest.ps1 b/Modules/CIPPCore/Public/New-CIPPDbRequest.ps1 index 3fa02a5998cf..0a3345e5cbac 100644 --- a/Modules/CIPPCore/Public/New-CIPPDbRequest.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPDbRequest.ps1 @@ -39,7 +39,19 @@ function New-CIPPDbRequest { $Table = Get-CippTable -tablename 'CippReportingDB' - $Tenant = Get-Tenants -TenantFilter $TenantFilter | Select-Object -ExpandProperty defaultDomainName + if (-not $script:CIPPDbRequestTenantCache) { + $script:CIPPDbRequestTenantCache = @{} + } + $CacheNow = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() + $CachedTenant = $script:CIPPDbRequestTenantCache[$TenantFilter] + if ($CachedTenant -and ($CacheNow - $CachedTenant.Timestamp) -lt 300) { + $Tenant = $CachedTenant.DefaultDomain + } else { + $Tenant = (Get-Tenants -TenantFilter $TenantFilter).defaultDomainName + if ($Tenant) { + $script:CIPPDbRequestTenantCache[$TenantFilter] = @{ DefaultDomain = $Tenant; Timestamp = $CacheNow } + } + } if (-not $Tenant) { if ($TenantFilter -eq $env:TenantID) { return $false diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDbCacheTestData.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDbCacheTestData.ps1 index 10302dd69d17..66fbf545949e 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDbCacheTestData.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDbCacheTestData.ps1 @@ -36,50 +36,62 @@ Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. '@ * 10 # Repeat to get ~3KB - $startTime = Get-Date + # Stopwatch is much cheaper than two Get-Date calls (Get-Date is documented in + # the PowerShell 7.4 performance guidance as a hot path to avoid). + $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() Write-Information "[Set-CIPPDbCacheTestData] Starting generation of $Count test objects for tenant $TenantFilter" - # Stream test objects directly to batch processor - 1..$Count | ForEach-Object { - [PSCustomObject]@{ - id = [guid]::NewGuid().ToString() - displayName = "Test User $_" - userPrincipalName = "testuser$_@$TenantFilter" - mail = "testuser$_@$TenantFilter" - givenName = 'Test' - surname = "User $_" - jobTitle = 'Test Engineer' - department = 'Testing Department' - officeLocation = "Test Office $_" - mobilePhone = "+1-555-000-$($_.ToString().PadLeft(4, '0'))" - businessPhones = @("+1-555-001-$($_.ToString().PadLeft(4, '0'))") - accountEnabled = $true - createdDateTime = (Get-Date).ToString('o') - lastSignInDateTime = (Get-Date).AddDays(-1).ToString('o') - description = $sampleText - companyName = 'Test Company' - country = 'United States' - city = 'Test City' - state = 'Test State' - postalCode = '12345' - streetAddress = '123 Test Street' - proxyAddresses = @("SMTP:testuser$_@$TenantFilter", "smtp:alias$_@$TenantFilter") - assignedLicenses = @( - @{ skuId = [guid]::NewGuid().ToString(); disabledPlans = @() } - ) - customAttribute1 = 'Custom Value 1' - customAttribute2 = 'Custom Value 2' - customAttribute3 = 'Custom Value 3' - additionalData = $sampleText + + # Stream test objects through the pipeline using `foreach` inside an inline + # scriptblock — preserves the original streaming behaviour (so Add-CIPPDbItem + # can flush in 500-row batches without holding all 50k rows in memory) while + # avoiding the per-iteration ForEach-Object scriptblock dispatch and + # PSCustomObject NoteProperty bag construction. Ordered hashtables serialize + # identically through ConvertTo-Json downstream. + & { + foreach ($i in 1..$Count) { + $padded = $i.ToString().PadLeft(4, '0') + [ordered]@{ + id = [guid]::NewGuid().ToString() + displayName = "Test User $i" + userPrincipalName = "testuser$i@$TenantFilter" + mail = "testuser$i@$TenantFilter" + givenName = 'Test' + surname = "User $i" + jobTitle = 'Test Engineer' + department = 'Testing Department' + officeLocation = "Test Office $i" + mobilePhone = "+1-555-000-$padded" + businessPhones = @("+1-555-001-$padded") + accountEnabled = $true + createdDateTime = (Get-Date).ToString('o') + lastSignInDateTime = (Get-Date).AddDays(-1).ToString('o') + description = $sampleText + companyName = 'Test Company' + country = 'United States' + city = 'Test City' + state = 'Test State' + postalCode = '12345' + streetAddress = '123 Test Street' + proxyAddresses = @("SMTP:testuser$i@$TenantFilter", "smtp:alias$i@$TenantFilter") + assignedLicenses = @( + @{ skuId = [guid]::NewGuid().ToString(); disabledPlans = @() } + ) + customAttribute1 = 'Custom Value 1' + customAttribute2 = 'Custom Value 2' + customAttribute3 = 'Custom Value 3' + additionalData = $sampleText + } } } | Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'TestData' -AddCount - $endTime = Get-Date - $duration = ($endTime - $startTime).TotalSeconds - $objectsPerSecond = [math]::Round($Count / $duration, 2) + $stopwatch.Stop() + $duration = $stopwatch.Elapsed.TotalSeconds + $objectsPerSecond = '{0:N2}' -f ($Count / $duration) + $durationFmt = '{0:N3}' -f $duration Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter ` - -message "Generated $Count test objects in $duration seconds ($objectsPerSecond objects/sec)" -sev Debug + -message "Generated $Count test objects in $durationFmt seconds ($objectsPerSecond objects/sec)" -sev Debug } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter ` diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_1.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_1.ps1 index 8fb0530f76f1..c5d1e600002c 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_1.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_1.ps1 @@ -20,10 +20,9 @@ function Invoke-CippTestCIS_1_1_1 { return } - $PrivilegedRoleIds = ($Roles | Where-Object { $_.isPrivileged -eq $true }).id - $PrivilegedAssignments = $RoleAssignments | Where-Object { $_.roleDefinitionId -in $PrivilegedRoleIds } - $PrivilegedUserIds = $PrivilegedAssignments.principalId | Select-Object -Unique - $PrivilegedUsers = $Users | Where-Object { $_.id -in $PrivilegedUserIds } + $PrivilegedRoleIds = [System.Collections.Generic.HashSet[string]]::new([string[]]$Roles.Where({ $_.isPrivileged -eq $true }).id) + $PrivilegedUserIds = [System.Collections.Generic.HashSet[string]]::new([string[]]($RoleAssignments.Where({ $PrivilegedRoleIds.Contains($_.roleDefinitionId) }).principalId | Select-Object -Unique)) + $PrivilegedUsers = $Users.Where({ $PrivilegedUserIds.Contains($_.id) }) if (-not $PrivilegedUsers) { Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_1_1_1' -TestType 'Identity' -Status 'Passed' -ResultMarkdown 'No privileged users found.' -Risk 'High' -Name 'Administrative accounts are cloud-only' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Privileged Access' @@ -38,13 +37,13 @@ function Invoke-CippTestCIS_1_1_1 { if ($NonCompliant.Count -eq 0) { $Status = 'Passed' - $Result = "All $($PrivilegedUsers.Count) privileged users are cloud-only and unlicensed." + $Result = [System.Text.StringBuilder]::new("All $($PrivilegedUsers.Count) privileged users are cloud-only and unlicensed.") } else { $Status = 'Failed' - $Result = "$($NonCompliant.Count) of $($PrivilegedUsers.Count) privileged user(s) are not cloud-only or are licensed:`n`n" - $Result += "| UPN | Synced | Licensed |`n| :-- | :----- | :------- |`n" + $Result = [System.Text.StringBuilder]::new("$($NonCompliant.Count) of $($PrivilegedUsers.Count) privileged user(s) are not cloud-only or are licensed:`n`n") + $null = $Result.Append("| UPN | Synced | Licensed |`n| :-- | :----- | :------- |`n") foreach ($U in ($NonCompliant | Select-Object -First 25)) { - $Result += "| $($U.userPrincipalName) | $([bool]$U.onPremisesSyncEnabled) | $([bool]($U.assignedLicenses.Count -gt 0)) |`n" + $null = $Result.Append("| $($U.userPrincipalName) | $([bool]$U.onPremisesSyncEnabled) | $([bool]($U.assignedLicenses.Count -gt 0)) |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_2.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_2.ps1 index 81849198254d..910a698a7063 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_2.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_2.ps1 @@ -19,16 +19,16 @@ function Invoke-CippTestCIS_1_1_2 { return } - $GA = $Roles | Where-Object { $_.displayName -eq 'Global Administrator' } | Select-Object -First 1 + $GA = $Roles.Where({ $_.displayName -eq 'Global Administrator' }, 'First', 1)[0] if (-not $GA) { Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_1_1_2' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'Global Administrator role not found in tenant role definitions.' -Risk 'High' -Name 'Two emergency access accounts have been defined' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Privileged Access' return } - $GAUserIds = ($RoleAssignments | Where-Object { $_.roleDefinitionId -eq $GA.id }).principalId - $GAUsers = $Users | Where-Object { $_.id -in $GAUserIds } + $GAUserIds = [System.Collections.Generic.HashSet[string]]::new([string[]]$RoleAssignments.Where({ $_.roleDefinitionId -eq $GA.id }).principalId) + $GAUsers = $Users.Where({ $GAUserIds.Contains($_.id) }) $BreakGlassPattern = 'breakglass|break-glass|emergency|cipp-bg|bg-admin' - $LikelyBG = $GAUsers | Where-Object { $_.userPrincipalName -match $BreakGlassPattern -and $_.onPremisesSyncEnabled -ne $true } + $LikelyBG = $GAUsers.Where({ $_.userPrincipalName -match $BreakGlassPattern -and $_.onPremisesSyncEnabled -ne $true }) if ($LikelyBG.Count -ge 2) { $Status = 'Passed' diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_4.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_4.ps1 index a0e446fbbef4..e33d80beba91 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_4.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_4.ps1 @@ -18,19 +18,19 @@ function Invoke-CippTestCIS_1_1_4 { # SkuPartNumbers that are acceptable for admin accounts: Entra ID P1/P2 only $AcceptableSkus = @('AAD_PREMIUM', 'AAD_PREMIUM_P2', 'EMS', 'EMSPREMIUM') - $PrivilegedRoleIds = ($Roles | Where-Object { $_.isPrivileged -eq $true }).id - $PrivilegedUserIds = ($RoleAssignments | Where-Object { $_.roleDefinitionId -in $PrivilegedRoleIds }).principalId | Select-Object -Unique - $PrivilegedUsers = $Users | Where-Object { $_.id -in $PrivilegedUserIds } + $PrivilegedRoleIds = [System.Collections.Generic.HashSet[string]]::new([string[]]$Roles.Where({ $_.isPrivileged -eq $true }).id) + $PrivilegedUserIds = [System.Collections.Generic.HashSet[string]]::new([string[]]($RoleAssignments.Where({ $PrivilegedRoleIds.Contains($_.roleDefinitionId) }).principalId | Select-Object -Unique)) + $PrivilegedUsers = $Users.Where({ $PrivilegedUserIds.Contains($_.id) }) - $LicensedAdmins = $PrivilegedUsers | Where-Object { + $LicensedAdmins = $PrivilegedUsers.Where({ $_.assignedLicenses -and $_.assignedLicenses.Count -gt 0 - } + }) - $NonCompliant = $LicensedAdmins | Where-Object { - $skus = ($_.assignedPlans | ForEach-Object { $_.servicePlanId }) -join ',' - $hasProductivity = $_.assignedPlans | Where-Object { $_.service -in @('exchange', 'SharePoint', 'MicrosoftCommunicationsOnline', 'TeamspaceAPI') -and $_.capabilityStatus -eq 'Enabled' } - [bool]$hasProductivity - } + $ProductivityServices = [System.Collections.Generic.HashSet[string]]::new([string[]]@('exchange', 'SharePoint', 'MicrosoftCommunicationsOnline', 'TeamspaceAPI')) + $NonCompliant = $LicensedAdmins.Where({ + $hasProductivity = $_.assignedPlans.Where({ $ProductivityServices.Contains($_.service) -and $_.capabilityStatus -eq 'Enabled' }, 'First', 1) + [bool]$hasProductivity.Count + }) if (-not $LicensedAdmins) { $Status = 'Passed' diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_2_1.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_2_1.ps1 index d35d3a2e9d8a..bc3b4679fbad 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_2_1.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_2_1.ps1 @@ -17,13 +17,13 @@ function Invoke-CippTestCIS_1_2_1 { if (-not $PublicGroups -or $PublicGroups.Count -eq 0) { $Status = 'Passed' - $Result = 'No public Microsoft 365 (Unified) groups found in the tenant.' + $Result = [System.Text.StringBuilder]::new('No public Microsoft 365 (Unified) groups found in the tenant.') } else { $Status = 'Failed' - $Result = "Found $($PublicGroups.Count) public Microsoft 365 group(s). Each public group's contents are visible to every user in the tenant — convert them to Private unless explicitly approved.`n`n" - $Result += "| Display Name | Mail |`n| :----------- | :--- |`n" + $Result = [System.Text.StringBuilder]::new("Found $($PublicGroups.Count) public Microsoft 365 group(s). Each public group's contents are visible to every user in the tenant — convert them to Private unless explicitly approved.`n`n") + $null = $Result.Append("| Display Name | Mail |`n| :----------- | :--- |`n") foreach ($G in ($PublicGroups | Select-Object -First 25)) { - $Result += "| $($G.displayName) | $($G.mail) |`n" + $null = $Result.Append("| $($G.displayName) | $($G.mail) |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_1.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_1.ps1 index 3b62354ad116..a18099e1a9c7 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_1.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_1.ps1 @@ -17,12 +17,12 @@ function Invoke-CippTestCIS_1_3_1 { if (-not $Failing -or $Failing.Count -eq 0) { $Status = 'Passed' - $Result = "All $($Domains.Count) domain(s) have password expiration disabled (passwordValidityPeriodInDays = 2147483647)." + $Result = [System.Text.StringBuilder]::new("All $($Domains.Count) domain(s) have password expiration disabled (passwordValidityPeriodInDays = 2147483647).") } else { $Status = 'Failed' - $Result = "$($Failing.Count) domain(s) still expire passwords:`n`n| Domain | Validity (days) |`n| :----- | :-------------- |`n" + $Result = [System.Text.StringBuilder]::new("$($Failing.Count) domain(s) still expire passwords:`n`n| Domain | Validity (days) |`n| :----- | :-------------- |`n") foreach ($D in $Failing) { - $Result += "| $($D.id) | $($D.passwordValidityPeriodInDays) |`n" + $null = $Result.Append("| $($D.id) | $($D.passwordValidityPeriodInDays) |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_10.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_10.ps1 index 4e0c321a5f93..3ad101ed07a1 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_10.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_10.ps1 @@ -19,12 +19,12 @@ function Invoke-CippTestCIS_2_1_10 { if (-not $Failing -or $Failing.Count -eq 0) { $Status = 'Passed' - $Result = "All $($Results.Count) domain(s) have a DMARC record with p=quarantine or p=reject." + $Result = [System.Text.StringBuilder]::new("All $($Results.Count) domain(s) have a DMARC record with p=quarantine or p=reject.") } else { $Status = 'Failed' - $Result = "$($Failing.Count) of $($Results.Count) domain(s) are missing a compliant DMARC record:`n`n| Domain | DMARCPresent | DMARCActionPolicy |`n| :----- | :----------- | :---------------- |`n" + $Result = [System.Text.StringBuilder]::new("$($Failing.Count) of $($Results.Count) domain(s) are missing a compliant DMARC record:`n`n| Domain | DMARCPresent | DMARCActionPolicy |`n| :----- | :----------- | :---------------- |`n") foreach ($D in ($Failing | Select-Object -First 25)) { - $Result += "| $($D.Domain) | $($D.DMARCPresent) | $($D.DMARCActionPolicy) |`n" + $null = $Result.Append("| $($D.Domain) | $($D.DMARCPresent) | $($D.DMARCActionPolicy) |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_14.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_14.ps1 index 6906ce5a406a..5a0ae74afa6d 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_14.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_14.ps1 @@ -17,12 +17,12 @@ function Invoke-CippTestCIS_2_1_14 { if (-not $Offending) { $Status = 'Passed' - $Result = "All $($Inbound.Count) inbound anti-spam policy/policies have no allowed sender domains." + $Result = [System.Text.StringBuilder]::new("All $($Inbound.Count) inbound anti-spam policy/policies have no allowed sender domains.") } else { $Status = 'Failed' - $Result = "$($Offending.Count) inbound anti-spam policy/policies have allowed sender domains configured:`n`n" + $Result = [System.Text.StringBuilder]::new("$($Offending.Count) inbound anti-spam policy/policies have allowed sender domains configured:`n`n") foreach ($P in $Offending) { - $Result += "- **$($P.Identity)**: $($P.AllowedSenderDomains -join ', ')`n" + $null = $Result.Append("- **$($P.Identity)**: $($P.AllowedSenderDomains -join ', ')`n") } } diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_8.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_8.ps1 index 82d0881cbe1a..b58ffa9664e7 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_8.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_8.ps1 @@ -17,12 +17,12 @@ function Invoke-CippTestCIS_2_1_8 { if (-not $Failing -or $Failing.Count -eq 0) { $Status = 'Passed' - $Result = "All $($Results.Count) domain(s) have an SPF record published." + $Result = [System.Text.StringBuilder]::new("All $($Results.Count) domain(s) have an SPF record published.") } else { $Status = 'Failed' - $Result = "$($Failing.Count) of $($Results.Count) domain(s) are missing a valid SPF record:`n`n| Domain | SPF Record |`n| :----- | :--------- |`n" + $Result = [System.Text.StringBuilder]::new("$($Failing.Count) of $($Results.Count) domain(s) are missing a valid SPF record:`n`n| Domain | SPF Record |`n| :----- | :--------- |`n") foreach ($D in ($Failing | Select-Object -First 25)) { - $Result += "| $($D.Domain) | $($D.ActualSPFRecord) |`n" + $null = $Result.Append("| $($D.Domain) | $($D.ActualSPFRecord) |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_9.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_9.ps1 index 3547c7403431..7cb90eabd3c9 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_9.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_9.ps1 @@ -25,11 +25,11 @@ function Invoke-CippTestCIS_2_1_9 { if ($Failed.Count -eq 0) { $Status = 'Passed' - $Result = "DKIM is enabled for all $($Sending.Count) sending domain(s)." + $Result = [System.Text.StringBuilder]::new("DKIM is enabled for all $($Sending.Count) sending domain(s).") } else { $Status = 'Failed' - $Result = "DKIM is not enabled for $($Failed.Count) sending domain(s):`n`n| Domain | DKIM Enabled |`n| :----- | :----------- |`n" - foreach ($F in $Failed) { $Result += "| $($F.Domain) | $($F.Enabled) |`n" } + $Result = [System.Text.StringBuilder]::new("DKIM is not enabled for $($Failed.Count) sending domain(s):`n`n| Domain | DKIM Enabled |`n| :----- | :----------- |`n") + foreach ($F in $Failed) { $null = $Result.Append("| $($F.Domain) | $($F.Enabled) |`n") } } Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_2_1_9' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'DKIM is enabled for all Exchange Online Domains' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Email Authentication' diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_1.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_1.ps1 index 7ea446f7a56f..730b29f55a04 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_1.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_1.ps1 @@ -14,15 +14,15 @@ function Invoke-CippTestCIS_5_2_2_1 { return } - $PrivRoleIds = ($Roles | Where-Object { $_.isPrivileged -eq $true }).id + $PrivRoleIds = [System.Collections.Generic.HashSet[string]]::new([string[]]$Roles.Where({ $_.isPrivileged -eq $true }).id) - $Matching = $CA | Where-Object { + $Matching = $CA.Where({ $_.state -eq 'enabled' -and $_.grantControls -and ($_.grantControls.builtInControls -contains 'mfa' -or $_.grantControls.authenticationStrength) -and $_.conditions.users.includeRoles -and - (@($_.conditions.users.includeRoles) | Where-Object { $_ -in $PrivRoleIds }).Count -gt 0 - } + ([string[]]$_.conditions.users.includeRoles).Where({ $PrivRoleIds.Contains($_) }, 'First', 1).Count -gt 0 + }) if ($Matching) { $Status = 'Passed' diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_4.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_4.ps1 index 5725d23ddd79..ea2462c3b457 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_4.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_4.ps1 @@ -14,18 +14,18 @@ function Invoke-CippTestCIS_5_2_2_4 { return } - $PrivRoleIds = ($Roles | Where-Object { $_.isPrivileged -eq $true }).id + $PrivRoleIds = [System.Collections.Generic.HashSet[string]]::new([string[]]$Roles.Where({ $_.isPrivileged -eq $true }).id) - $Matching = $CA | Where-Object { + $Matching = $CA.Where({ $_.state -eq 'enabled' -and $_.conditions.users.includeRoles -and - (@($_.conditions.users.includeRoles) | Where-Object { $_ -in $PrivRoleIds }).Count -gt 0 -and + ([string[]]$_.conditions.users.includeRoles).Where({ $PrivRoleIds.Contains($_) }, 'First', 1).Count -gt 0 -and $_.sessionControls -and $_.sessionControls.signInFrequency -and $_.sessionControls.signInFrequency.isEnabled -eq $true -and $_.sessionControls.persistentBrowser -and $_.sessionControls.persistentBrowser.mode -eq 'never' - } + }) if ($Matching) { $Status = 'Passed' diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_3_1.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_3_1.ps1 index cd591efbb4aa..935bbf9d7c76 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_3_1.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_3_1.ps1 @@ -15,8 +15,8 @@ function Invoke-CippTestCIS_5_3_1 { return } - $PrivRoleIds = ($Roles | Where-Object { $_.isPrivileged -eq $true }).id - $EligibleAssignmentsForPriv = $Eligibility | Where-Object { $_.roleDefinitionId -in $PrivRoleIds } + $PrivRoleIds = [System.Collections.Generic.HashSet[string]]::new([string[]]$Roles.Where({ $_.isPrivileged -eq $true }).id) + $EligibleAssignmentsForPriv = $Eligibility.Where({ $PrivRoleIds.Contains($_.roleDefinitionId) }) if ($EligibleAssignmentsForPriv -and $EligibleAssignmentsForPriv.Count -gt 0) { $Status = 'Passed' diff --git a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO101.ps1 b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO101.ps1 index 3280623b0a6a..3ad60d1ad77f 100644 --- a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO101.ps1 +++ b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO101.ps1 @@ -26,14 +26,14 @@ function Invoke-CippTestCISAMSEXO101 { $FailedPolicies = $MalwarePolicies | Where-Object { -not $_.EnableFileFilter } if ($FailedPolicies.Count -eq 0) { - $Result = "✅ **Pass**: All $($MalwarePolicies.Count) malware filter policy/policies have file filtering enabled." + $Result = [System.Text.StringBuilder]::new("✅ **Pass**: All $($MalwarePolicies.Count) malware filter policy/policies have file filtering enabled.") $Status = 'Passed' } else { - $Result = "❌ **Fail**: $($FailedPolicies.Count) of $($MalwarePolicies.Count) malware filter policy/policies do not have file filtering enabled:`n`n" - $Result += "| Policy Name | File Filter Enabled |`n" - $Result += "| :---------- | :------------------ |`n" + $Result = [System.Text.StringBuilder]::new("❌ **Fail**: $($FailedPolicies.Count) of $($MalwarePolicies.Count) malware filter policy/policies do not have file filtering enabled:`n`n") + $null = $Result.Append("| Policy Name | File Filter Enabled |`n") + $null = $Result.Append("| :---------- | :------------------ |`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.Name) | $($Policy.EnableFileFilter) |`n" + $null = $Result.Append("| $($Policy.Name) | $($Policy.EnableFileFilter) |`n") } $Status = 'Failed' } diff --git a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO102.ps1 b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO102.ps1 index ec9f77655698..e1ab88df20d4 100644 --- a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO102.ps1 +++ b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO102.ps1 @@ -37,14 +37,14 @@ function Invoke-CippTestCISAMSEXO102 { } if ($FailedPolicies.Count -eq 0) { - $Result = "✅ **Pass**: All $($MalwarePolicies.Count) malware filter policy/policies quarantine or delete emails with malware." + $Result = [System.Text.StringBuilder]::new("✅ **Pass**: All $($MalwarePolicies.Count) malware filter policy/policies quarantine or delete emails with malware.") $Status = 'Passed' } else { - $Result = "❌ **Fail**: $($FailedPolicies.Count) of $($MalwarePolicies.Count) malware filter policy/policies do not quarantine or delete malware:`n`n" - $Result += "| Policy Name | Current Action | Expected |`n" - $Result += "| :---------- | :------------- | :------- |`n" + $Result = [System.Text.StringBuilder]::new("❌ **Fail**: $($FailedPolicies.Count) of $($MalwarePolicies.Count) malware filter policy/policies do not quarantine or delete malware:`n`n") + $null = $Result.Append("| Policy Name | Current Action | Expected |`n") + $null = $Result.Append("| :---------- | :------------- | :------- |`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.'Policy Name') | $($Policy.'Current Action') | $($Policy.Expected) |`n" + $null = $Result.Append("| $($Policy.'Policy Name') | $($Policy.'Current Action') | $($Policy.Expected) |`n") } $Status = 'Failed' } diff --git a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO103.ps1 b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO103.ps1 index 0362875758df..0ced07fd39aa 100644 --- a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO103.ps1 +++ b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO103.ps1 @@ -26,14 +26,14 @@ function Invoke-CippTestCISAMSEXO103 { $FailedPolicies = $MalwarePolicies | Where-Object { -not $_.ZapEnabled } if ($FailedPolicies.Count -eq 0) { - $Result = "✅ **Pass**: All $($MalwarePolicies.Count) malware filter policy/policies have ZAP (Zero-hour Auto Purge) enabled." + $Result = [System.Text.StringBuilder]::new("✅ **Pass**: All $($MalwarePolicies.Count) malware filter policy/policies have ZAP (Zero-hour Auto Purge) enabled.") $Status = 'Passed' } else { - $Result = "❌ **Fail**: $($FailedPolicies.Count) of $($MalwarePolicies.Count) malware filter policy/policies do not have ZAP enabled:`n`n" - $Result += "| Policy Name | ZAP Enabled |`n" - $Result += "| :---------- | :---------- |`n" + $Result = [System.Text.StringBuilder]::new("❌ **Fail**: $($FailedPolicies.Count) of $($MalwarePolicies.Count) malware filter policy/policies do not have ZAP enabled:`n`n") + $null = $Result.Append("| Policy Name | ZAP Enabled |`n") + $null = $Result.Append("| :---------- | :---------- |`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.Name) | $($Policy.ZapEnabled) |`n" + $null = $Result.Append("| $($Policy.Name) | $($Policy.ZapEnabled) |`n") } $Status = 'Failed' } diff --git a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO11.ps1 b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO11.ps1 index 5519f0e2b9bd..c400638593a3 100644 --- a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO11.ps1 +++ b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO11.ps1 @@ -26,14 +26,14 @@ function Invoke-CippTestCISAMSEXO11 { $ForwardingEnabledDomains = $RemoteDomains | Where-Object { $_.AutoForwardEnabled -eq $true } if (($ForwardingEnabledDomains | Measure-Object).Count -eq 0) { - $Result = '✅ **Pass**: Automatic forwarding to external domains is disabled for all remote domains.' + $Result = [System.Text.StringBuilder]::new('✅ **Pass**: Automatic forwarding to external domains is disabled for all remote domains.') $Status = 'Passed' } else { - $Result = "❌ **Fail**: $($ForwardingEnabledDomains.Count) domain(s) have automatic forwarding enabled:`n`n" - $Result += "| Domain Name | Auto Forward |`n" - $Result += "| :---------- | :----------- |`n" + $Result = [System.Text.StringBuilder]::new("❌ **Fail**: $($ForwardingEnabledDomains.Count) domain(s) have automatic forwarding enabled:`n`n") + $null = $Result.Append("| Domain Name | Auto Forward |`n") + $null = $Result.Append("| :---------- | :----------- |`n") foreach ($Domain in $ForwardingEnabledDomains) { - $Result += "| $($Domain.DomainName) | $($Domain.AutoForwardEnabled) |`n" + $null = $Result.Append("| $($Domain.DomainName) | $($Domain.AutoForwardEnabled) |`n") } $Status = 'Failed' } diff --git a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO112.ps1 b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO112.ps1 index ebc6b5f8e3ee..60f74ec92b92 100644 --- a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO112.ps1 +++ b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO112.ps1 @@ -30,16 +30,16 @@ function Invoke-CippTestCISAMSEXO112 { } if ($PoliciesWithTips.Count -gt 0) { - $Result = "✅ **Pass**: $($PoliciesWithTips.Count) policy/policies have impersonation safety tips enabled:`n`n" - $Result += "| Policy | Similar Users Tips | Similar Domains Tips | Unusual Characters Tips |`n" - $Result += "| :----- | :----------------- | :------------------- | :---------------------- |`n" + $Result = [System.Text.StringBuilder]::new("✅ **Pass**: $($PoliciesWithTips.Count) policy/policies have impersonation safety tips enabled:`n`n") + $null = $Result.Append("| Policy | Similar Users Tips | Similar Domains Tips | Unusual Characters Tips |`n") + $null = $Result.Append("| :----- | :----------------- | :------------------- | :---------------------- |`n") foreach ($Policy in $PoliciesWithTips) { - $Result += "| $($Policy.Identity) | $($Policy.EnableSimilarUsersSafetyTips) | $($Policy.EnableSimilarDomainsSafetyTips) | $($Policy.EnableUnusualCharactersSafetyTips) |`n" + $null = $Result.Append("| $($Policy.Identity) | $($Policy.EnableSimilarUsersSafetyTips) | $($Policy.EnableSimilarDomainsSafetyTips) | $($Policy.EnableUnusualCharactersSafetyTips) |`n") } $Status = 'Passed' } else { - $Result = "❌ **Fail**: No policies found with impersonation safety tips enabled.`n`n" - $Result += "Enable safety tips in preset security policies to warn users about potential impersonation." + $Result = [System.Text.StringBuilder]::new("❌ **Fail**: No policies found with impersonation safety tips enabled.`n`n") + $null = $Result.Append("Enable safety tips in preset security policies to warn users about potential impersonation.") $Status = 'Failed' } diff --git a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO113.ps1 b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO113.ps1 index 68d40d45bd03..555983192cda 100644 --- a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO113.ps1 +++ b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO113.ps1 @@ -29,16 +29,16 @@ function Invoke-CippTestCISAMSEXO113 { } if ($PoliciesWithIntelligence.Count -gt 0) { - $Result = "✅ **Pass**: $($PoliciesWithIntelligence.Count) policy/policies have mailbox intelligence enabled:`n`n" - $Result += "| Policy | Mailbox Intelligence | Intelligence Protection | State |`n" - $Result += "| :----- | :------------------- | :---------------------- | :---- |`n" + $Result = [System.Text.StringBuilder]::new("✅ **Pass**: $($PoliciesWithIntelligence.Count) policy/policies have mailbox intelligence enabled:`n`n") + $null = $Result.Append("| Policy | Mailbox Intelligence | Intelligence Protection | State |`n") + $null = $Result.Append("| :----- | :------------------- | :---------------------- | :---- |`n") foreach ($Policy in $PoliciesWithIntelligence) { - $Result += "| $($Policy.Identity) | $($Policy.EnableMailboxIntelligence) | $($Policy.EnableMailboxIntelligenceProtection) | $($Policy.State) |`n" + $null = $Result.Append("| $($Policy.Identity) | $($Policy.EnableMailboxIntelligence) | $($Policy.EnableMailboxIntelligenceProtection) | $($Policy.State) |`n") } $Status = 'Passed' } else { - $Result = "❌ **Fail**: No policies found with mailbox intelligence enabled.`n`n" - $Result += 'Enable mailbox intelligence in preset security policies for AI-powered impersonation protection.' + $Result = [System.Text.StringBuilder]::new("❌ **Fail**: No policies found with mailbox intelligence enabled.`n`n") + $null = $Result.Append('Enable mailbox intelligence in preset security policies for AI-powered impersonation protection.') $Status = 'Failed' } diff --git a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO121.ps1 b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO121.ps1 index dd6333788ebd..076946c66a31 100644 --- a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO121.ps1 +++ b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO121.ps1 @@ -26,18 +26,18 @@ function Invoke-CippTestCISAMSEXO121 { $AllowedSenders = $AllowBlockList | Where-Object { $_.Action -eq 'Allow' -and $_.ListType -eq 'Sender' } if ($AllowedSenders.Count -eq 0) { - $Result = '✅ **Pass**: No allowed senders configured in tenant allow/block list.' + $Result = [System.Text.StringBuilder]::new('✅ **Pass**: No allowed senders configured in tenant allow/block list.') $Status = 'Passed' } else { - $Result = "❌ **Fail**: $($AllowedSenders.Count) allowed sender(s) configured in tenant allow/block list" + $Result = [System.Text.StringBuilder]::new("❌ **Fail**: $($AllowedSenders.Count) allowed sender(s) configured in tenant allow/block list") if ($AllowedSenders.Count -gt 10) { - $Result += ' (showing first 10)' + $null = $Result.Append(' (showing first 10)') } - $Result += ":`n`n" - $Result += "| Value | Action | List Type |`n" - $Result += "| :---- | :----- | :-------- |`n" + $null = $Result.Append(":`n`n") + $null = $Result.Append("| Value | Action | List Type |`n") + $null = $Result.Append("| :---- | :----- | :-------- |`n") foreach ($Sender in ($AllowedSenders | Select-Object -First 10)) { - $Result += "| $($Sender.Value) | $($Sender.Action) | $($Sender.ListType) |`n" + $null = $Result.Append("| $($Sender.Value) | $($Sender.Action) | $($Sender.ListType) |`n") } $Status = 'Failed' } diff --git a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO122.ps1 b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO122.ps1 index ea0c5c5e020d..f84e33a94501 100644 --- a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO122.ps1 +++ b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO122.ps1 @@ -26,14 +26,14 @@ function Invoke-CippTestCISAMSEXO122 { $FailedPolicies = $SpamPolicies | Where-Object { $_.EnableSafeList -eq $true } if ($FailedPolicies.Count -eq 0) { - $Result = "✅ **Pass**: All $($SpamPolicies.Count) anti-spam policy/policies have safe lists disabled." + $Result = [System.Text.StringBuilder]::new("✅ **Pass**: All $($SpamPolicies.Count) anti-spam policy/policies have safe lists disabled.") $Status = 'Passed' } else { - $Result = "❌ **Fail**: $($FailedPolicies.Count) of $($SpamPolicies.Count) anti-spam policy/policies have safe lists enabled:`n`n" - $Result += "| Policy Name | Safe List Enabled |`n" - $Result += "| :---------- | :---------------- |`n" + $Result = [System.Text.StringBuilder]::new("❌ **Fail**: $($FailedPolicies.Count) of $($SpamPolicies.Count) anti-spam policy/policies have safe lists enabled:`n`n") + $null = $Result.Append("| Policy Name | Safe List Enabled |`n") + $null = $Result.Append("| :---------- | :---------------- |`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.Name) | $($Policy.EnableSafeList) |`n" + $null = $Result.Append("| $($Policy.Name) | $($Policy.EnableSafeList) |`n") } $Status = 'Failed' } diff --git a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO131.ps1 b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO131.ps1 index 0a80171888ad..167153bfefd2 100644 --- a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO131.ps1 +++ b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO131.ps1 @@ -26,12 +26,12 @@ function Invoke-CippTestCISAMSEXO131 { $OrgConfigObject = $OrgConfig | Select-Object -First 1 if ($OrgConfigObject.AuditDisabled -eq $false) { - $Result = '✅ **Pass**: Mailbox auditing is enabled for the organization.' + $Result = [System.Text.StringBuilder]::new('✅ **Pass**: Mailbox auditing is enabled for the organization.') $Status = 'Passed' } else { - $Result = "❌ **Fail**: Mailbox auditing is disabled for the organization.`n`n" - $Result += "**Current Setting:**`n" - $Result += "- AuditDisabled: $($OrgConfigObject.AuditDisabled)" + $Result = [System.Text.StringBuilder]::new("❌ **Fail**: Mailbox auditing is disabled for the organization.`n`n") + $null = $Result.Append("**Current Setting:**`n") + $null = $Result.Append("- AuditDisabled: $($OrgConfigObject.AuditDisabled)") $Status = 'Failed' } diff --git a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO141.ps1 b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO141.ps1 index 2eca3acff798..b4968c25a793 100644 --- a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO141.ps1 +++ b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO141.ps1 @@ -26,14 +26,14 @@ function Invoke-CippTestCISAMSEXO141 { $FailedPolicies = $SpamPolicies | Where-Object { $_.HighConfidenceSpamAction -ne 'Quarantine' } if ($FailedPolicies.Count -eq 0) { - $Result = "✅ **Pass**: All $($SpamPolicies.Count) anti-spam policy/policies quarantine high confidence spam." + $Result = [System.Text.StringBuilder]::new("✅ **Pass**: All $($SpamPolicies.Count) anti-spam policy/policies quarantine high confidence spam.") $Status = 'Passed' } else { - $Result = "❌ **Fail**: $($FailedPolicies.Count) of $($SpamPolicies.Count) anti-spam policy/policies do not quarantine high confidence spam:`n`n" - $Result += "| Policy Name | Current Action | Expected |`n" - $Result += "| :---------- | :------------- | :------- |`n" + $Result = [System.Text.StringBuilder]::new("❌ **Fail**: $($FailedPolicies.Count) of $($SpamPolicies.Count) anti-spam policy/policies do not quarantine high confidence spam:`n`n") + $null = $Result.Append("| Policy Name | Current Action | Expected |`n") + $null = $Result.Append("| :---------- | :------------- | :------- |`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.'Policy Name') | $($Policy.'Current Action') | $($Policy.Expected) |`n" + $null = $Result.Append("| $($Policy.'Policy Name') | $($Policy.'Current Action') | $($Policy.Expected) |`n") } $Status = 'Failed' } diff --git a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO142.ps1 b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO142.ps1 index 3d6477aabc04..79104ff941f7 100644 --- a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO142.ps1 +++ b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO142.ps1 @@ -37,14 +37,14 @@ function Invoke-CippTestCISAMSEXO142 { } if ($FailedPolicies.Count -eq 0) { - $Result = "✅ **Pass**: All $($SpamPolicies.Count) anti-spam policy/policies move spam to junk folder or quarantine." + $Result = [System.Text.StringBuilder]::new("✅ **Pass**: All $($SpamPolicies.Count) anti-spam policy/policies move spam to junk folder or quarantine.") $Status = 'Passed' } else { - $Result = "❌ **Fail**: $($FailedPolicies.Count) of $($SpamPolicies.Count) anti-spam policy/policies do not properly handle spam:`n`n" - $Result += "| Policy Name | Current Action | Expected |`n" - $Result += "| :---------- | :------------- | :------- |`n" + $Result = [System.Text.StringBuilder]::new("❌ **Fail**: $($FailedPolicies.Count) of $($SpamPolicies.Count) anti-spam policy/policies do not properly handle spam:`n`n") + $null = $Result.Append("| Policy Name | Current Action | Expected |`n") + $null = $Result.Append("| :---------- | :------------- | :------- |`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.'Policy Name') | $($Policy.'Current Action') | $($Policy.Expected) |`n" + $null = $Result.Append("| $($Policy.'Policy Name') | $($Policy.'Current Action') | $($Policy.Expected) |`n") } $Status = 'Failed' } diff --git a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO143.ps1 b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO143.ps1 index 0605186a6e7b..21af91add0af 100644 --- a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO143.ps1 +++ b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO143.ps1 @@ -40,14 +40,14 @@ function Invoke-CippTestCISAMSEXO143 { } if ($FailedPolicies.Count -eq 0) { - $Result = "✅ **Pass**: All $($SpamPolicies.Count) anti-spam policy/policies have no spam filter bypasses configured." + $Result = [System.Text.StringBuilder]::new("✅ **Pass**: All $($SpamPolicies.Count) anti-spam policy/policies have no spam filter bypasses configured.") $Status = 'Passed' } else { - $Result = "❌ **Fail**: $($FailedPolicies.Count) of $($SpamPolicies.Count) anti-spam policy/policies have spam filter bypasses configured:`n`n" - $Result += "| Policy Name | Allowed Senders | Allowed Domains | Issue |`n" - $Result += "| :---------- | :-------------- | :-------------- | :---- |`n" + $Result = [System.Text.StringBuilder]::new("❌ **Fail**: $($FailedPolicies.Count) of $($SpamPolicies.Count) anti-spam policy/policies have spam filter bypasses configured:`n`n") + $null = $Result.Append("| Policy Name | Allowed Senders | Allowed Domains | Issue |`n") + $null = $Result.Append("| :---------- | :-------------- | :-------------- | :---- |`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.'Policy Name') | $($Policy.'Allowed Senders') | $($Policy.'Allowed Domains') | $($Policy.Issue) |`n" + $null = $Result.Append("| $($Policy.'Policy Name') | $($Policy.'Allowed Senders') | $($Policy.'Allowed Domains') | $($Policy.Issue) |`n") } $Status = 'Failed' } diff --git a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO151.ps1 b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO151.ps1 index cbc4f460951c..7a487dfcdc41 100644 --- a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO151.ps1 +++ b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO151.ps1 @@ -26,14 +26,14 @@ function Invoke-CippTestCISAMSEXO151 { $FailedPolicies = $SafeLinksPolicies | Where-Object { -not $_.EnableSafeLinksForEmail } if ($FailedPolicies.Count -eq 0) { - $Result = "✅ **Pass**: All $($SafeLinksPolicies.Count) Safe Links policy/policies have URL comparison with block-list enabled." + $Result = [System.Text.StringBuilder]::new("✅ **Pass**: All $($SafeLinksPolicies.Count) Safe Links policy/policies have URL comparison with block-list enabled.") $Status = 'Passed' } else { - $Result = "❌ **Fail**: $($FailedPolicies.Count) of $($SafeLinksPolicies.Count) Safe Links policy/policies do not have URL scanning enabled:`n`n" - $Result += "| Policy Name | Safe Links for Email |`n" - $Result += "| :---------- | :------------------- |`n" + $Result = [System.Text.StringBuilder]::new("❌ **Fail**: $($FailedPolicies.Count) of $($SafeLinksPolicies.Count) Safe Links policy/policies do not have URL scanning enabled:`n`n") + $null = $Result.Append("| Policy Name | Safe Links for Email |`n") + $null = $Result.Append("| :---------- | :------------------- |`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.Name) | $($Policy.EnableSafeLinksForEmail) |`n" + $null = $Result.Append("| $($Policy.Name) | $($Policy.EnableSafeLinksForEmail) |`n") } $Status = 'Failed' } diff --git a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO152.ps1 b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO152.ps1 index 88a246cf1374..e051dcb7f6a6 100644 --- a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO152.ps1 +++ b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO152.ps1 @@ -26,14 +26,14 @@ function Invoke-CippTestCISAMSEXO152 { $FailedPolicies = $SafeLinksPolicies | Where-Object { -not $_.ScanUrls } if ($FailedPolicies.Count -eq 0) { - $Result = "✅ **Pass**: All $($SafeLinksPolicies.Count) Safe Links policy/policies have real-time URL scanning enabled." + $Result = [System.Text.StringBuilder]::new("✅ **Pass**: All $($SafeLinksPolicies.Count) Safe Links policy/policies have real-time URL scanning enabled.") $Status = 'Passed' } else { - $Result = "❌ **Fail**: $($FailedPolicies.Count) of $($SafeLinksPolicies.Count) Safe Links policy/policies do not have real-time URL scanning enabled:`n`n" - $Result += "| Policy Name | Scan URLs |`n" - $Result += "| :---------- | :-------- |`n" + $Result = [System.Text.StringBuilder]::new("❌ **Fail**: $($FailedPolicies.Count) of $($SafeLinksPolicies.Count) Safe Links policy/policies do not have real-time URL scanning enabled:`n`n") + $null = $Result.Append("| Policy Name | Scan URLs |`n") + $null = $Result.Append("| :---------- | :-------- |`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.Name) | $($Policy.ScanUrls) |`n" + $null = $Result.Append("| $($Policy.Name) | $($Policy.ScanUrls) |`n") } $Status = 'Failed' } diff --git a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO153.ps1 b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO153.ps1 index 5e413ca22c00..ed50fec8a0f0 100644 --- a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO153.ps1 +++ b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO153.ps1 @@ -26,14 +26,14 @@ function Invoke-CippTestCISAMSEXO153 { $FailedPolicies = $SafeLinksPolicies | Where-Object { $_.TrackUserClicks -eq $true } if ($FailedPolicies.Count -eq 0) { - $Result = "✅ **Pass**: All $($SafeLinksPolicies.Count) Safe Links policy/policies have click tracking disabled." + $Result = [System.Text.StringBuilder]::new("✅ **Pass**: All $($SafeLinksPolicies.Count) Safe Links policy/policies have click tracking disabled.") $Status = 'Passed' } else { - $Result = "❌ **Fail**: $($FailedPolicies.Count) of $($SafeLinksPolicies.Count) Safe Links policy/policies have click tracking enabled:`n`n" - $Result += "| Policy Name | Track User Clicks |`n" - $Result += "| :---------- | :---------------- |`n" + $Result = [System.Text.StringBuilder]::new("❌ **Fail**: $($FailedPolicies.Count) of $($SafeLinksPolicies.Count) Safe Links policy/policies have click tracking enabled:`n`n") + $null = $Result.Append("| Policy Name | Track User Clicks |`n") + $null = $Result.Append("| :---------- | :---------------- |`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.Name) | $($Policy.TrackUserClicks) |`n" + $null = $Result.Append("| $($Policy.Name) | $($Policy.TrackUserClicks) |`n") } $Status = 'Failed' } diff --git a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO171.ps1 b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO171.ps1 index 9ec61ec96a6f..06fe3ca4c264 100644 --- a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO171.ps1 +++ b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO171.ps1 @@ -26,14 +26,14 @@ function Invoke-CippTestCISAMSEXO171 { $AuditConfigObject = $AuditConfig | Select-Object -First 1 if ($AuditConfigObject.UnifiedAuditLogIngestionEnabled -eq $true) { - $Result = "✅ **Pass**: Microsoft Purview Audit (Standard) logging is enabled.`n`n" - $Result += "**Current Settings:**`n" - $Result += "- UnifiedAuditLogIngestionEnabled: $($AuditConfigObject.UnifiedAuditLogIngestionEnabled)" + $Result = [System.Text.StringBuilder]::new("✅ **Pass**: Microsoft Purview Audit (Standard) logging is enabled.`n`n") + $null = $Result.Append("**Current Settings:**`n") + $null = $Result.Append("- UnifiedAuditLogIngestionEnabled: $($AuditConfigObject.UnifiedAuditLogIngestionEnabled)") $Status = 'Passed' } else { - $Result = "❌ **Fail**: Microsoft Purview Audit (Standard) logging is not enabled.`n`n" - $Result += "**Current Settings:**`n" - $Result += "- UnifiedAuditLogIngestionEnabled: $($AuditConfigObject.UnifiedAuditLogIngestionEnabled)" + $Result = [System.Text.StringBuilder]::new("❌ **Fail**: Microsoft Purview Audit (Standard) logging is not enabled.`n`n") + $null = $Result.Append("**Current Settings:**`n") + $null = $Result.Append("- UnifiedAuditLogIngestionEnabled: $($AuditConfigObject.UnifiedAuditLogIngestionEnabled)") $Status = 'Failed' } diff --git a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO173.ps1 b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO173.ps1 index c7bdfae5f0dc..ea4780621b3a 100644 --- a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO173.ps1 +++ b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO173.ps1 @@ -26,14 +26,14 @@ function Invoke-CippTestCISAMSEXO173 { $AuditConfigObject = $AuditConfig | Select-Object -First 1 if ($AuditConfigObject.AdminAuditLogEnabled -eq $true) { - $Result = "✅ **Pass**: Admin audit log is enabled (provides 1 year retention).`n`n" - $Result += "**Current Settings:**`n" - $Result += "- AdminAuditLogEnabled: $($AuditConfigObject.AdminAuditLogEnabled)" + $Result = [System.Text.StringBuilder]::new("✅ **Pass**: Admin audit log is enabled (provides 1 year retention).`n`n") + $null = $Result.Append("**Current Settings:**`n") + $null = $Result.Append("- AdminAuditLogEnabled: $($AuditConfigObject.AdminAuditLogEnabled)") $Status = 'Passed' } else { - $Result = "❌ **Fail**: Admin audit log is not enabled.`n`n" - $Result += "**Current Settings:**`n" - $Result += "- AdminAuditLogEnabled: $($AuditConfigObject.AdminAuditLogEnabled)" + $Result = [System.Text.StringBuilder]::new("❌ **Fail**: Admin audit log is not enabled.`n`n") + $null = $Result.Append("**Current Settings:**`n") + $null = $Result.Append("- AdminAuditLogEnabled: $($AuditConfigObject.AdminAuditLogEnabled)") $Status = 'Failed' } diff --git a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO31.ps1 b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO31.ps1 index f1ddaff3ef16..a547857bd91c 100644 --- a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO31.ps1 +++ b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO31.ps1 @@ -47,14 +47,14 @@ function Invoke-CippTestCISAMSEXO31 { } if ($FailedDomains.Count -eq 0) { - $Result = "✅ **Pass**: DKIM is enabled for all $($SendingDomains.Count) sending domain(s)." + $Result = [System.Text.StringBuilder]::new("✅ **Pass**: DKIM is enabled for all $($SendingDomains.Count) sending domain(s).") $Status = 'Passed' } else { - $Result = "❌ **Fail**: $($FailedDomains.Count) of $($SendingDomains.Count) domain(s) do not have DKIM properly enabled:`n`n" - $Result += "| Domain | DKIM Enabled | Status |`n" - $Result += "| :----- | :----------- | :----- |`n" + $Result = [System.Text.StringBuilder]::new("❌ **Fail**: $($FailedDomains.Count) of $($SendingDomains.Count) domain(s) do not have DKIM properly enabled:`n`n") + $null = $Result.Append("| Domain | DKIM Enabled | Status |`n") + $null = $Result.Append("| :----- | :----------- | :----- |`n") foreach ($Domain in $FailedDomains) { - $Result += "| $($Domain.Domain) | $($Domain.'DKIM Enabled') | $($Domain.Status) |`n" + $null = $Result.Append("| $($Domain.Domain) | $($Domain.'DKIM Enabled') | $($Domain.Status) |`n") } $Status = 'Failed' } diff --git a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO51.ps1 b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO51.ps1 index 9061f5b258e7..3c513b43d8bc 100644 --- a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO51.ps1 +++ b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO51.ps1 @@ -26,18 +26,18 @@ function Invoke-CippTestCISAMSEXO51 { $FailedMailboxes = $CASMailboxes | Where-Object { $_.SmtpClientAuthenticationDisabled -eq $false } if ($FailedMailboxes.Count -eq 0) { - $Result = "✅ **Pass**: SMTP authentication is disabled for all $($CASMailboxes.Count) mailbox(es)." + $Result = [System.Text.StringBuilder]::new("✅ **Pass**: SMTP authentication is disabled for all $($CASMailboxes.Count) mailbox(es).") $Status = 'Passed' } else { - $Result = "❌ **Fail**: $($FailedMailboxes.Count) of $($CASMailboxes.Count) mailbox(es) have SMTP authentication enabled" + $Result = [System.Text.StringBuilder]::new("❌ **Fail**: $($FailedMailboxes.Count) of $($CASMailboxes.Count) mailbox(es) have SMTP authentication enabled") if ($FailedMailboxes.Count -gt 10) { - $Result += ' (showing first 10)' + $null = $Result.Append(' (showing first 10)') } - $Result += ":`n`n" - $Result += "| Display Name | Identity | SMTP Auth Disabled |`n" - $Result += "| :----------- | :------- | :----------------- |`n" + $null = $Result.Append(":`n`n") + $null = $Result.Append("| Display Name | Identity | SMTP Auth Disabled |`n") + $null = $Result.Append("| :----------- | :------- | :----------------- |`n") foreach ($Mailbox in ($FailedMailboxes | Select-Object -First 10)) { - $Result += "| $($Mailbox.DisplayName) | $($Mailbox.Identity) | $($Mailbox.SmtpClientAuthenticationDisabled) |`n" + $null = $Result.Append("| $($Mailbox.DisplayName) | $($Mailbox.Identity) | $($Mailbox.SmtpClientAuthenticationDisabled) |`n") } $Status = 'Failed' } diff --git a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO61.ps1 b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO61.ps1 index 3a42e55c563c..6614818ec038 100644 --- a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO61.ps1 +++ b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO61.ps1 @@ -40,14 +40,14 @@ function Invoke-CippTestCISAMSEXO61 { } if ($FailedPolicies.Count -eq 0) { - $Result = '✅ **Pass**: No sharing policies allow contact folder sharing with external domains.' + $Result = [System.Text.StringBuilder]::new('✅ **Pass**: No sharing policies allow contact folder sharing with external domains.') $Status = 'Passed' } else { - $Result = "❌ **Fail**: $($FailedPolicies.Count) sharing policy/policies allow contact folder sharing:`n`n" - $Result += "| Policy Name | Enabled | Issue |`n" - $Result += "| :---------- | :------ | :---- |`n" + $Result = [System.Text.StringBuilder]::new("❌ **Fail**: $($FailedPolicies.Count) sharing policy/policies allow contact folder sharing:`n`n") + $null = $Result.Append("| Policy Name | Enabled | Issue |`n") + $null = $Result.Append("| :---------- | :------ | :---- |`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.'Policy Name') | $($Policy.Enabled) | $($Policy.Issue) |`n" + $null = $Result.Append("| $($Policy.'Policy Name') | $($Policy.Enabled) | $($Policy.Issue) |`n") } $Status = 'Failed' } diff --git a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO62.ps1 b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO62.ps1 index 021fd0c9680b..12f304da818d 100644 --- a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO62.ps1 +++ b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO62.ps1 @@ -41,14 +41,14 @@ function Invoke-CippTestCISAMSEXO62 { } if ($FailedPolicies.Count -eq 0) { - $Result = "✅ **Pass**: No sharing policies allow detailed calendar sharing with all domains." + $Result = [System.Text.StringBuilder]::new("✅ **Pass**: No sharing policies allow detailed calendar sharing with all domains.") $Status = 'Passed' } else { - $Result = "❌ **Fail**: $($FailedPolicies.Count) sharing policy/policies allow detailed calendar sharing with all domains:`n`n" - $Result += "| Policy Name | Enabled | Issue |`n" - $Result += "| :---------- | :------ | :---- |`n" + $Result = [System.Text.StringBuilder]::new("❌ **Fail**: $($FailedPolicies.Count) sharing policy/policies allow detailed calendar sharing with all domains:`n`n") + $null = $Result.Append("| Policy Name | Enabled | Issue |`n") + $null = $Result.Append("| :---------- | :------ | :---- |`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.'Policy Name') | $($Policy.Enabled) | $($Policy.Issue) |`n" + $null = $Result.Append("| $($Policy.'Policy Name') | $($Policy.Enabled) | $($Policy.Issue) |`n") } $Status = 'Failed' } diff --git a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO71.ps1 b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO71.ps1 index 17880ef45290..6951858c3e5b 100644 --- a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO71.ps1 +++ b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO71.ps1 @@ -26,12 +26,12 @@ function Invoke-CippTestCISAMSEXO71 { $OrgConfigObject = $OrgConfig | Select-Object -First 1 if ($OrgConfigObject.ExternalInOutlook -eq $true) { - $Result = '✅ **Pass**: External sender warnings are enabled in Outlook.' + $Result = [System.Text.StringBuilder]::new('✅ **Pass**: External sender warnings are enabled in Outlook.') $Status = 'Passed' } else { - $Result = "❌ **Fail**: External sender warnings are not enabled in Outlook.`n`n" - $Result += "**Current Setting:**`n" - $Result += "- ExternalInOutlook: $($OrgConfigObject.ExternalInOutlook)" + $Result = [System.Text.StringBuilder]::new("❌ **Fail**: External sender warnings are not enabled in Outlook.`n`n") + $null = $Result.Append("**Current Setting:**`n") + $null = $Result.Append("- ExternalInOutlook: $($OrgConfigObject.ExternalInOutlook)") $Status = 'Failed' } diff --git a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO95.ps1 b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO95.ps1 index 5b7433097bdc..00acb7805e6c 100644 --- a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO95.ps1 +++ b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO95.ps1 @@ -51,16 +51,16 @@ function Invoke-CippTestCISAMSEXO95 { } if ($FailedPolicies.Count -eq 0) { - $Result = '✅ **Pass**: All malware filter policies block click-to-run files (.exe, .cmd, .vbe).' + $Result = [System.Text.StringBuilder]::new('✅ **Pass**: All malware filter policies block click-to-run files (.exe, .cmd, .vbe).') $Status = 'Passed' } else { - $Result = "❌ **Fail**: $($FailedPolicies.Count) malware filter policy/policies do not properly block click-to-run executables:`n`n" - $Result += "| Policy Name | File Filter Enabled | Missing Blocked Types |`n" - $Result += "| :---------- | :------------------ | :-------------------- |`n" + $Result = [System.Text.StringBuilder]::new("❌ **Fail**: $($FailedPolicies.Count) malware filter policy/policies do not properly block click-to-run executables:`n`n") + $null = $Result.Append("| Policy Name | File Filter Enabled | Missing Blocked Types |`n") + $null = $Result.Append("| :---------- | :------------------ | :-------------------- |`n") foreach ($Policy in $FailedPolicies) { $fileFilterValue = if ($Policy.'File Filter Enabled') { $Policy.'File Filter Enabled' } else { $Policy.'Issue' } $missingTypes = if ($Policy.'Missing Blocked Types') { $Policy.'Missing Blocked Types' } else { 'N/A' } - $Result += "| $($Policy.'Policy Name') | $fileFilterValue | $missingTypes |`n" + $null = $Result.Append("| $($Policy.'Policy Name') | $fileFilterValue | $missingTypes |`n") } $Status = 'Failed' } diff --git a/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady001.ps1 b/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady001.ps1 index fb7dd130ff2d..983269e10c8d 100644 --- a/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady001.ps1 +++ b/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady001.ps1 @@ -33,17 +33,17 @@ function Invoke-CippTestCopilotReady001 { if ($EligibleSkus.Count -gt 0) { $Status = 'Passed' - $Result = "Tenant has **$($EligibleSkus.Count)** eligible prerequisite license plan(s) covering **$AssignableCount** seats that qualify for Microsoft 365 Copilot.`n`n" - $Result += "| License | Total Seats | Assigned |`n" - $Result += "|---------|------------|---------|`n" + $Result = [System.Text.StringBuilder]::new("Tenant has **$($EligibleSkus.Count)** eligible prerequisite license plan(s) covering **$AssignableCount** seats that qualify for Microsoft 365 Copilot.`n`n") + $null = $Result.Append("| License | Total Seats | Assigned |`n") + $null = $Result.Append("|---------|------------|---------|`n") foreach ($Sku in $EligibleSkus) { - $Result += "| $($Sku.License) | $($Sku.TotalLicenses) | $($Sku.CountUsed) |`n" + $null = $Result.Append("| $($Sku.License) | $($Sku.TotalLicenses) | $($Sku.CountUsed) |`n") } } else { $Status = 'Failed' - $Result = "No Microsoft 365 Copilot prerequisite licenses were found in this tenant.`n`n" - $Result += 'Users must have an eligible M365 plan before a Copilot add-on license can be assigned. ' - $Result += 'See [Microsoft licensing requirements](https://learn.microsoft.com/en-us/copilot/microsoft-365/microsoft-365-copilot-licensing) for the full list.' + $Result = [System.Text.StringBuilder]::new("No Microsoft 365 Copilot prerequisite licenses were found in this tenant.`n`n") + $null = $Result.Append('Users must have an eligible M365 plan before a Copilot add-on license can be assigned. ') + $null = $Result.Append('See [Microsoft licensing requirements](https://learn.microsoft.com/en-us/copilot/microsoft-365/microsoft-365-copilot-licensing) for the full list.') } Add-CippTestResult -TenantFilter $Tenant -TestId 'CopilotReady001' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Tenant has M365 Copilot prerequisite licenses' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Copilot Readiness' diff --git a/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady002.ps1 b/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady002.ps1 index 2be9ef3320ad..01542ec1dc53 100644 --- a/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady002.ps1 +++ b/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady002.ps1 @@ -38,28 +38,28 @@ function Invoke-CippTestCopilotReady002 { if ($CopilotLicenses.Count -eq 0) { $Status = 'Failed' - $Result = "No Microsoft 365 Copilot add-on licenses were found in this tenant.`n`n" - $Result += 'Purchase Microsoft 365 Copilot licenses and assign them to eligible users to enable Copilot features.' + $Result = [System.Text.StringBuilder]::new("No Microsoft 365 Copilot add-on licenses were found in this tenant.`n`n") + $null = $Result.Append('Purchase Microsoft 365 Copilot licenses and assign them to eligible users to enable Copilot features.') } elseif ($TotalConsumed -eq 0) { $Status = 'Failed' - $Result = "Microsoft 365 Copilot licenses exist (**$TotalEnabled** seats) but **none are assigned** to any users.`n`n" - $Result += "| License | Total Seats | Assigned | Available |`n" - $Result += "|---------|------------|----------|-----------|`n" + $Result = [System.Text.StringBuilder]::new("Microsoft 365 Copilot licenses exist (**$TotalEnabled** seats) but **none are assigned** to any users.`n`n") + $null = $Result.Append("| License | Total Seats | Assigned | Available |`n") + $null = $Result.Append("|---------|------------|----------|-----------|`n") foreach ($Sku in $CopilotLicenses) { $Available = [int]$Sku.TotalLicenses - [int]$Sku.CountUsed - $Result += "| $($Sku.License) | $($Sku.TotalLicenses) | $($Sku.CountUsed) | $Available |`n" + $null = $Result.Append("| $($Sku.License) | $($Sku.TotalLicenses) | $($Sku.CountUsed) | $Available |`n") } } else { $Status = 'Passed' - $Result = "Microsoft 365 Copilot licenses are purchased and assigned.`n`n" - $Result += "| License | Total Seats | Assigned | Available |`n" - $Result += "|---------|------------|----------|-----------|`n" + $Result = [System.Text.StringBuilder]::new("Microsoft 365 Copilot licenses are purchased and assigned.`n`n") + $null = $Result.Append("| License | Total Seats | Assigned | Available |`n") + $null = $Result.Append("|---------|------------|----------|-----------|`n") foreach ($Sku in $CopilotLicenses) { $Available = [int]$Sku.TotalLicenses - [int]$Sku.CountUsed - $Result += "| $($Sku.License) | $($Sku.TotalLicenses) | $($Sku.CountUsed) | $Available |`n" + $null = $Result.Append("| $($Sku.License) | $($Sku.TotalLicenses) | $($Sku.CountUsed) | $Available |`n") } if ($TotalAvailable -gt 0) { - $Result += "`n**$TotalAvailable unassigned seat(s)** are available to assign to additional users." + $null = $Result.Append("`n**$TotalAvailable unassigned seat(s)** are available to assign to additional users.") } } diff --git a/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady003.ps1 b/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady003.ps1 index aa2fc5f723fa..a73b499233e0 100644 --- a/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady003.ps1 +++ b/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady003.ps1 @@ -80,15 +80,15 @@ function Invoke-CippTestCopilotReady003 { if ($DesktopPercent -ge $DesktopThresholdPercent) { $Status = 'Passed' - $Result = "**$DesktopCount of $TotalUsers licensed users ($DesktopPercent%)** have Microsoft 365 Apps activated on a desktop platform (Windows or Mac) — above the $DesktopThresholdPercent% threshold.`n`n" - $Result += "These users can access Copilot features in desktop Word, Excel, PowerPoint, and Outlook." + $Result = [System.Text.StringBuilder]::new("**$DesktopCount of $TotalUsers licensed users ($DesktopPercent%)** have Microsoft 365 Apps activated on a desktop platform (Windows or Mac) — above the $DesktopThresholdPercent% threshold.`n`n") + $null = $Result.Append("These users can access Copilot features in desktop Word, Excel, PowerPoint, and Outlook.") } else { $Status = 'Failed' - $Result = "Only **$DesktopCount of $TotalUsers licensed users ($DesktopPercent%)** have Microsoft 365 Apps activated on a desktop platform — below the $DesktopThresholdPercent% threshold.`n`n" - $Result += "Copilot in Word, Excel, PowerPoint, and Outlook requires the M365 desktop application. " - $Result += "Users with only web or mobile activations, or who have never activated at all, cannot use Copilot's in-document features.`n`n" + $Result = [System.Text.StringBuilder]::new("Only **$DesktopCount of $TotalUsers licensed users ($DesktopPercent%)** have Microsoft 365 Apps activated on a desktop platform — below the $DesktopThresholdPercent% threshold.`n`n") + $null = $Result.Append("Copilot in Word, Excel, PowerPoint, and Outlook requires the M365 desktop application. ") + $null = $Result.Append("Users with only web or mobile activations, or who have never activated at all, cannot use Copilot's in-document features.`n`n") if ($NoDesktopUsers.Count -gt 0 -and $NoDesktopUsers.Count -le 20) { - $Result += "**Users without desktop activation:**`n" + $null = $Result.Append("**Users without desktop activation:**`n") foreach ($User in $NoDesktopUsers) { if ($User.neverActivated) { $PlatformStr = ' (never activated)' @@ -97,13 +97,13 @@ function Invoke-CippTestCopilotReady003 { if ([int]($User.android ?? 0) -gt 0 -or [int]($User.ios ?? 0) -gt 0) { $Platforms += 'Mobile' } $PlatformStr = if ($Platforms) { " ($(($Platforms -join ', ')) only)" } else { ' (no activations)' } } - $Result += "- $($User.displayName) ($($User.userPrincipalName))$PlatformStr`n" + $null = $Result.Append("- $($User.displayName) ($($User.userPrincipalName))$PlatformStr`n") } } elseif ($NoDesktopUsers.Count -gt 20) { $NeverActivated = @($NoDesktopUsers | Where-Object { $_.neverActivated }).Count - $Result += "**$($NoDesktopUsers.Count) users** have no desktop M365 Apps activation" - if ($NeverActivated -gt 0) { $Result += " ($NeverActivated have never activated on any platform)" } - $Result += ".`n" + $null = $Result.Append("**$($NoDesktopUsers.Count) users** have no desktop M365 Apps activation") + if ($NeverActivated -gt 0) { $null = $Result.Append(" ($NeverActivated have never activated on any platform)") } + $null = $Result.Append(".`n") } } diff --git a/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady004.ps1 b/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady004.ps1 index 721bb9058bd8..519029d6747d 100644 --- a/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady004.ps1 +++ b/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady004.ps1 @@ -56,18 +56,18 @@ function Invoke-CippTestCopilotReady004 { if ($ActivityPercent -ge $ActivityThresholdPercent) { $Status = 'Passed' - $Result = "**$ActiveCount of $TotalUsers licensed users ($ActivityPercent%)** sent email in the past 30 days — above the $ActivityThresholdPercent% threshold.`n`n" - $Result += 'These users are good candidates for Copilot in Outlook, which provides AI-assisted drafting, summarization, and email coaching.' + $Result = [System.Text.StringBuilder]::new("**$ActiveCount of $TotalUsers licensed users ($ActivityPercent%)** sent email in the past 30 days — above the $ActivityThresholdPercent% threshold.`n`n") + $null = $Result.Append('These users are good candidates for Copilot in Outlook, which provides AI-assisted drafting, summarization, and email coaching.') } else { $Status = 'Failed' - $Result = "Only **$ActiveCount of $TotalUsers licensed users ($ActivityPercent%)** sent email in the past 30 days — below the $ActivityThresholdPercent% threshold.`n`n" - $Result += 'Copilot for Outlook delivers the most value to active email users. ' - $Result += "Consider reviewing Exchange Online license assignment and adoption before rolling out Copilot.`n`n" + $Result = [System.Text.StringBuilder]::new("Only **$ActiveCount of $TotalUsers licensed users ($ActivityPercent%)** sent email in the past 30 days — below the $ActivityThresholdPercent% threshold.`n`n") + $null = $Result.Append('Copilot for Outlook delivers the most value to active email users. ') + $null = $Result.Append("Consider reviewing Exchange Online license assignment and adoption before rolling out Copilot.`n`n") if ($InactiveUsers.Count -gt 0 -and $InactiveUsers.Count -le 20) { - $Result += "**Inactive users (no Outlook email in 30 days):**`n" - foreach ($Upn in $InactiveUsers) { $Result += "- $Upn`n" } + $null = $Result.Append("**Inactive users (no Outlook email in 30 days):**`n") + foreach ($Upn in $InactiveUsers) { $null = $Result.Append("- $Upn`n") } } elseif ($InactiveUsers.Count -gt 20) { - $Result += "**$($InactiveUsers.Count) users** had no Outlook email activity in the past 30 days." + $null = $Result.Append("**$($InactiveUsers.Count) users** had no Outlook email activity in the past 30 days.") } } diff --git a/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady005.ps1 b/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady005.ps1 index c8ffe38a60f9..a91dfc76d8ee 100644 --- a/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady005.ps1 +++ b/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady005.ps1 @@ -56,18 +56,18 @@ function Invoke-CippTestCopilotReady005 { if ($ActivityPercent -ge $ActivityThresholdPercent) { $Status = 'Passed' - $Result = "**$ActiveCount of $TotalUsers licensed users ($ActivityPercent%)** used Teams meetings or chat in the past 30 days — above the $ActivityThresholdPercent% threshold.`n`n" - $Result += 'These users are strong candidates for Copilot in Teams, which provides meeting summaries, chat recaps, and real-time meeting assistance.' + $Result = [System.Text.StringBuilder]::new("**$ActiveCount of $TotalUsers licensed users ($ActivityPercent%)** used Teams meetings or chat in the past 30 days — above the $ActivityThresholdPercent% threshold.`n`n") + $null = $Result.Append('These users are strong candidates for Copilot in Teams, which provides meeting summaries, chat recaps, and real-time meeting assistance.') } else { $Status = 'Failed' - $Result = "Only **$ActiveCount of $TotalUsers licensed users ($ActivityPercent%)** used Teams meetings or chat in the past 30 days — below the $ActivityThresholdPercent% threshold.`n`n" - $Result += 'Copilot for Teams delivers the most value to users who regularly use chat and meetings. ' - $Result += "Consider driving Teams adoption before or alongside a Copilot rollout.`n`n" + $Result = [System.Text.StringBuilder]::new("Only **$ActiveCount of $TotalUsers licensed users ($ActivityPercent%)** used Teams meetings or chat in the past 30 days — below the $ActivityThresholdPercent% threshold.`n`n") + $null = $Result.Append('Copilot for Teams delivers the most value to users who regularly use chat and meetings. ') + $null = $Result.Append("Consider driving Teams adoption before or alongside a Copilot rollout.`n`n") if ($InactiveUsers.Count -gt 0 -and $InactiveUsers.Count -le 20) { - $Result += "**Inactive users (no Teams activity in 30 days):**`n" - foreach ($Upn in $InactiveUsers) { $Result += "- $Upn`n" } + $null = $Result.Append("**Inactive users (no Teams activity in 30 days):**`n") + foreach ($Upn in $InactiveUsers) { $null = $Result.Append("- $Upn`n") } } elseif ($InactiveUsers.Count -gt 20) { - $Result += "**$($InactiveUsers.Count) users** had no Teams meetings or chat activity in the past 30 days." + $null = $Result.Append("**$($InactiveUsers.Count) users** had no Teams meetings or chat activity in the past 30 days.") } } diff --git a/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady006.ps1 b/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady006.ps1 index be6c118d3328..fd6e5a4616e5 100644 --- a/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady006.ps1 +++ b/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady006.ps1 @@ -56,18 +56,18 @@ function Invoke-CippTestCopilotReady006 { if ($ActivityPercent -ge $ActivityThresholdPercent) { $Status = 'Passed' - $Result = "**$ActiveCount of $TotalUsers licensed users ($ActivityPercent%)** worked on OneDrive or SharePoint files in the past 30 days \u2014 above the $ActivityThresholdPercent% threshold.`n`n" - $Result += 'These users are strong candidates for Copilot, which provides the most value when users actively collaborate on files in Microsoft 365.' + $Result = [System.Text.StringBuilder]::new("**$ActiveCount of $TotalUsers licensed users ($ActivityPercent%)** worked on OneDrive or SharePoint files in the past 30 days \u2014 above the $ActivityThresholdPercent% threshold.`n`n") + $null = $Result.Append('These users are strong candidates for Copilot, which provides the most value when users actively collaborate on files in Microsoft 365.') } else { $Status = 'Failed' - $Result = "Only **$ActiveCount of $TotalUsers licensed users ($ActivityPercent%)** worked on OneDrive or SharePoint files in the past 30 days \u2014 below the $ActivityThresholdPercent% threshold.`n`n" - $Result += 'Copilot delivers the most value when users regularly store and collaborate on files in OneDrive and SharePoint. ' - $Result += "Consider driving file collaboration adoption before or alongside a Copilot rollout.`n`n" + $Result = [System.Text.StringBuilder]::new("Only **$ActiveCount of $TotalUsers licensed users ($ActivityPercent%)** worked on OneDrive or SharePoint files in the past 30 days \u2014 below the $ActivityThresholdPercent% threshold.`n`n") + $null = $Result.Append('Copilot delivers the most value when users regularly store and collaborate on files in OneDrive and SharePoint. ') + $null = $Result.Append("Consider driving file collaboration adoption before or alongside a Copilot rollout.`n`n") if ($InactiveUsers.Count -gt 0 -and $InactiveUsers.Count -le 20) { - $Result += "**Inactive users (no Office doc activity in 30 days):**`n" - foreach ($Upn in $InactiveUsers) { $Result += "- $Upn`n" } + $null = $Result.Append("**Inactive users (no Office doc activity in 30 days):**`n") + foreach ($Upn in $InactiveUsers) { $null = $Result.Append("- $Upn`n") } } elseif ($InactiveUsers.Count -gt 20) { - $Result += "**$($InactiveUsers.Count) users** had no OneDrive or SharePoint file activity in the past 30 days." + $null = $Result.Append("**$($InactiveUsers.Count) users** had no OneDrive or SharePoint file activity in the past 30 days.") } } diff --git a/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady007.ps1 b/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady007.ps1 index 5e67952c4017..a3dff2755827 100644 --- a/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady007.ps1 +++ b/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady007.ps1 @@ -59,19 +59,19 @@ function Invoke-CippTestCopilotReady007 { if ($ChannelPercent -ge $ChannelThresholdPercent) { $Status = 'Passed' - $Result = "**$QualifiedCount of $TotalUsers M365 Apps licensed users ($ChannelPercent%)** are on Current Channel or Monthly Enterprise Channel — above the $ChannelThresholdPercent% threshold.`n`n" - $Result += 'These users will receive Copilot feature updates for desktop Word, Excel, PowerPoint, Outlook, and OneNote.' + $Result = [System.Text.StringBuilder]::new("**$QualifiedCount of $TotalUsers M365 Apps licensed users ($ChannelPercent%)** are on Current Channel or Monthly Enterprise Channel — above the $ChannelThresholdPercent% threshold.`n`n") + $null = $Result.Append('These users will receive Copilot feature updates for desktop Word, Excel, PowerPoint, Outlook, and OneNote.') } else { $Status = 'Failed' - $Result = "Only **$QualifiedCount of $TotalUsers M365 Apps licensed users ($ChannelPercent%)** are on a qualified update channel — below the $ChannelThresholdPercent% threshold.`n`n" - $Result += 'Copilot in M365 desktop apps requires **Current Channel** or **Monthly Enterprise Channel**. ' - $Result += "Users on Semi-Annual Enterprise Channel or other update rings will not receive Copilot features.`n`n" - $Result += "To remediate, update the Microsoft 365 Apps update channel via Microsoft Intune, Microsoft 365 admin center, or Group Policy.`n`n" + $Result = [System.Text.StringBuilder]::new("Only **$QualifiedCount of $TotalUsers M365 Apps licensed users ($ChannelPercent%)** are on a qualified update channel — below the $ChannelThresholdPercent% threshold.`n`n") + $null = $Result.Append('Copilot in M365 desktop apps requires **Current Channel** or **Monthly Enterprise Channel**. ') + $null = $Result.Append("Users on Semi-Annual Enterprise Channel or other update rings will not receive Copilot features.`n`n") + $null = $Result.Append("To remediate, update the Microsoft 365 Apps update channel via Microsoft Intune, Microsoft 365 admin center, or Group Policy.`n`n") if ($NotQualifiedUsers.Count -gt 0 -and $NotQualifiedUsers.Count -le 20) { - $Result += "**Users not on a qualified update channel:**`n" - foreach ($Upn in $NotQualifiedUsers) { $Result += "- $Upn`n" } + $null = $Result.Append("**Users not on a qualified update channel:**`n") + foreach ($Upn in $NotQualifiedUsers) { $null = $Result.Append("- $Upn`n") } } elseif ($NotQualifiedUsers.Count -gt 20) { - $Result += "**$($NotQualifiedUsers.Count) users** are not on a qualified update channel." + $null = $Result.Append("**$($NotQualifiedUsers.Count) users** are not on a qualified update channel.") } } diff --git a/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady008.ps1 b/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady008.ps1 index fd4254a173cf..79cf9ee11344 100644 --- a/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady008.ps1 +++ b/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady008.ps1 @@ -69,20 +69,20 @@ function Invoke-CippTestCopilotReady008 { $MedPct = [math]::Round(($MediumTier.Count / $Total) * 100, 1) $LowPct = [math]::Round(($LowTier.Count / $Total) * 100, 1) - $Result = "## Copilot Candidate Tier Breakdown`n`n" - $Result += "Scoring is based on 6 readiness signals from the Microsoft 365 Copilot Readiness report (30-day window).`n`n" - $Result += "| Tier | Users | % of Tenant | Description |`n" - $Result += "|------|-------|-------------|-------------|`n" - $Result += "| **High** (≥4 signals) | $($HighTier.Count) | $HighPct% | Power M365 users — strongest Copilot ROI |`n" - $Result += "| **Medium** (3 signals) | $($MediumTier.Count) | $MedPct% | Engaged users — good Copilot candidates |`n" - $Result += "| **Low** (≤2 signals) | $($LowTier.Count) | $LowPct% | Low engagement — adopt M365 basics first |`n" - $Result += "`n**Signals scored:** Copilot license assigned, qualified update channel, Teams meetings, Teams chat, Outlook email, Office documents (each = 1 point)`n" + $Result = [System.Text.StringBuilder]::new("## Copilot Candidate Tier Breakdown`n`n") + $null = $Result.Append("Scoring is based on 6 readiness signals from the Microsoft 365 Copilot Readiness report (30-day window).`n`n") + $null = $Result.Append("| Tier | Users | % of Tenant | Description |`n") + $null = $Result.Append("|------|-------|-------------|-------------|`n") + $null = $Result.Append("| **High** (≥4 signals) | $($HighTier.Count) | $HighPct% | Power M365 users — strongest Copilot ROI |`n") + $null = $Result.Append("| **Medium** (3 signals) | $($MediumTier.Count) | $MedPct% | Engaged users — good Copilot candidates |`n") + $null = $Result.Append("| **Low** (≤2 signals) | $($LowTier.Count) | $LowPct% | Low engagement — adopt M365 basics first |`n") + $null = $Result.Append("`n**Signals scored:** Copilot license assigned, qualified update channel, Teams meetings, Teams chat, Outlook email, Office documents (each = 1 point)`n") if ($HighTier.Count -gt 0 -and $HighTier.Count -le 20) { - $Result += "`n**High tier users:**`n" - foreach ($Upn in $HighTier) { $Result += "- $Upn`n" } + $null = $Result.Append("`n**High tier users:**`n") + foreach ($Upn in $HighTier) { $null = $Result.Append("- $Upn`n") } } elseif ($HighTier.Count -gt 20) { - $Result += "`n*$($HighTier.Count) users are in the high tier — use the Microsoft 365 Copilot Readiness report in the admin center for the full list.*`n" + $null = $Result.Append("`n*$($HighTier.Count) users are in the high tier — use the Microsoft 365 Copilot Readiness report in the admin center for the full list.*`n") } Add-CippTestResult -TenantFilter $Tenant -TestId 'CopilotReady008' -TestType 'Identity' -Status 'Informational' -ResultMarkdown $Result -Risk 'Informational' -Name 'Copilot candidate tier breakdown' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Copilot Readiness' diff --git a/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady009.ps1 b/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady009.ps1 index d98182851d1f..c02db723fd5d 100644 --- a/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady009.ps1 +++ b/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady009.ps1 @@ -57,13 +57,13 @@ function Invoke-CippTestCopilotReady009 { if ($ReadyPercent -ge $AdoptionThresholdPercent) { $Status = 'Passed' - $Result = "**$MediumOrAbove of $Total licensed users ($ReadyPercent%)** score Medium or above on Copilot readiness signals — above the $AdoptionThresholdPercent% threshold.`n`n" - $Result += 'This tenant has strong M365 engagement across the user base and is well-positioned for a Copilot rollout.' + $Result = [System.Text.StringBuilder]::new("**$MediumOrAbove of $Total licensed users ($ReadyPercent%)** score Medium or above on Copilot readiness signals — above the $AdoptionThresholdPercent% threshold.`n`n") + $null = $Result.Append('This tenant has strong M365 engagement across the user base and is well-positioned for a Copilot rollout.') } else { $Status = 'Failed' - $Result = "Only **$MediumOrAbove of $Total licensed users ($ReadyPercent%)** score Medium or above on Copilot readiness signals — below the $AdoptionThresholdPercent% threshold.`n`n" - $Result += "**$LowCount users** have low M365 engagement (≤2 of 6 signals). Copilot delivers the most value where users are already active across Teams, Outlook, and Office apps.`n`n" - $Result += "Consider running an M365 adoption campaign — focused on Teams meetings, Teams chat, Outlook, and OneDrive/SharePoint file usage — before or alongside a Copilot rollout.`n`n" + $Result = [System.Text.StringBuilder]::new("Only **$MediumOrAbove of $Total licensed users ($ReadyPercent%)** score Medium or above on Copilot readiness signals — below the $AdoptionThresholdPercent% threshold.`n`n") + $null = $Result.Append("**$LowCount users** have low M365 engagement (≤2 of 6 signals). Copilot delivers the most value where users are already active across Teams, Outlook, and Office apps.`n`n") + $null = $Result.Append("Consider running an M365 adoption campaign — focused on Teams meetings, Teams chat, Outlook, and OneDrive/SharePoint file usage — before or alongside a Copilot rollout.`n`n") } Add-CippTestResult -TenantFilter $Tenant -TestId 'CopilotReady009' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Majority of users are Copilot-ready (Medium or above)' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Copilot Readiness' diff --git a/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady010.ps1 b/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady010.ps1 index b12a6eadb155..86989d192ce0 100644 --- a/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady010.ps1 +++ b/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady010.ps1 @@ -52,17 +52,17 @@ function Invoke-CippTestCopilotReady010 { if ($NotRegistered.Count -eq 0) { $Status = 'Passed' - $Result = "All **$Total licensed users** have MFA registered — the tenant meets the MFA security baseline for Copilot deployment." + $Result = [System.Text.StringBuilder]::new("All **$Total licensed users** have MFA registered — the tenant meets the MFA security baseline for Copilot deployment.") } else { $Status = 'Failed' - $Result = "**$($NotRegistered.Count) of $Total licensed users ($([math]::Round(($NotRegistered.Count / $Total) * 100, 1))%)** do not have MFA registered.`n`n" - $Result += 'MFA is a security baseline requirement before deploying Copilot. Accounts without MFA present elevated risk when Copilot has access to tenant data.`n`n' - $Result += "Remediate by enforcing MFA via Conditional Access or per-user MFA, and requiring users to register via [aka.ms/mfasetup](https://aka.ms/mfasetup).`n`n" + $Result = [System.Text.StringBuilder]::new("**$($NotRegistered.Count) of $Total licensed users ($([math]::Round(($NotRegistered.Count / $Total) * 100, 1))%)** do not have MFA registered.`n`n") + $null = $Result.Append('MFA is a security baseline requirement before deploying Copilot. Accounts without MFA present elevated risk when Copilot has access to tenant data.`n`n') + $null = $Result.Append("Remediate by enforcing MFA via Conditional Access or per-user MFA, and requiring users to register via [aka.ms/mfasetup](https://aka.ms/mfasetup).`n`n") if ($NotRegistered.Count -le 20) { - $Result += "**Users without MFA registered:**`n" - foreach ($Upn in $NotRegistered) { $Result += "- $Upn`n" } + $null = $Result.Append("**Users without MFA registered:**`n") + foreach ($Upn in $NotRegistered) { $null = $Result.Append("- $Upn`n") } } else { - $Result += "**$($NotRegistered.Count) users** do not have MFA registered." + $null = $Result.Append("**$($NotRegistered.Count) users** do not have MFA registered.") } } diff --git a/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady011.ps1 b/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady011.ps1 index 064ead6b549c..8348dd2f1ae8 100644 --- a/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady011.ps1 +++ b/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady011.ps1 @@ -23,24 +23,24 @@ function Invoke-CippTestCopilotReady011 { if ($EnabledPolicies.Count -gt 0) { $Status = 'Passed' - $Result = "**$($EnabledPolicies.Count) enabled Conditional Access polic$(if ($EnabledPolicies.Count -eq 1) { 'y' } else { 'ies' })** found in the tenant.`n`n" - $Result += "| Policy Name | State |`n" - $Result += "|-------------|-------|`n" + $Result = [System.Text.StringBuilder]::new("**$($EnabledPolicies.Count) enabled Conditional Access polic$(if ($EnabledPolicies.Count -eq 1) { 'y' } else { 'ies' })** found in the tenant.`n`n") + $null = $Result.Append("| Policy Name | State |`n") + $null = $Result.Append("|-------------|-------|`n") foreach ($Policy in ($EnabledPolicies | Sort-Object displayName)) { - $Result += "| $($Policy.displayName) | Enabled |`n" + $null = $Result.Append("| $($Policy.displayName) | Enabled |`n") } if ($ReportOnlyPolicies.Count -gt 0) { - $Result += "`n*$($ReportOnlyPolicies.Count) additional polic$(if ($ReportOnlyPolicies.Count -eq 1) { 'y is' } else { 'ies are' }) in report-only mode and not enforcing access controls.*" + $null = $Result.Append("`n*$($ReportOnlyPolicies.Count) additional polic$(if ($ReportOnlyPolicies.Count -eq 1) { 'y is' } else { 'ies are' }) in report-only mode and not enforcing access controls.*") } } else { $Status = 'Failed' - $Result = "No enabled Conditional Access policies were found in this tenant.`n`n" + $Result = [System.Text.StringBuilder]::new("No enabled Conditional Access policies were found in this tenant.`n`n") if ($ReportOnlyPolicies.Count -gt 0) { - $Result += "**$($ReportOnlyPolicies.Count) polic$(if ($ReportOnlyPolicies.Count -eq 1) { 'y is' } else { 'ies are' }) in report-only mode** but not enforcing.`n`n" + $null = $Result.Append("**$($ReportOnlyPolicies.Count) polic$(if ($ReportOnlyPolicies.Count -eq 1) { 'y is' } else { 'ies are' }) in report-only mode** but not enforcing.`n`n") } - $Result += 'Conditional Access is the primary mechanism for enforcing MFA, device compliance, and access controls in Entra ID. ' - $Result += 'Before deploying Copilot, establish at least a baseline CA policy requiring MFA for all users. ' - $Result += 'See [Microsoft CA policy templates](https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-policy-common) to get started.' + $null = $Result.Append('Conditional Access is the primary mechanism for enforcing MFA, device compliance, and access controls in Entra ID. ') + $null = $Result.Append('Before deploying Copilot, establish at least a baseline CA policy requiring MFA for all users. ') + $null = $Result.Append('See [Microsoft CA policy templates](https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-policy-common) to get started.') } Add-CippTestResult -TenantFilter $Tenant -TestId 'CopilotReady011' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Tenant has enabled Conditional Access policies' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Copilot Readiness' diff --git a/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady012.ps1 b/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady012.ps1 index 6e3a2e5023d0..ea6a590526c3 100644 --- a/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady012.ps1 +++ b/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady012.ps1 @@ -47,21 +47,21 @@ function Invoke-CippTestCopilotReady012 { if ($Issues.Count -eq 0) { $Status = 'Passed' - $Result = "All user self-service creation permissions are restricted — users cannot create groups, tenants, or register applications without admin involvement.`n`n" - $Result += 'This reduces shadow IT risk and ensures governance controls apply to new M365 resources before Copilot can interact with them.' + $Result = [System.Text.StringBuilder]::new("All user self-service creation permissions are restricted — users cannot create groups, tenants, or register applications without admin involvement.`n`n") + $null = $Result.Append('This reduces shadow IT risk and ensures governance controls apply to new M365 resources before Copilot can interact with them.') } else { $Status = 'Failed' - $Result = "**$($Issues.Count) user permission$(if ($Issues.Count -eq 1) { '' } else { 's' })** allow unrestricted self-service creation.`n`n" - $Result += "| Permission | Status |`n" - $Result += "|------------|--------|`n" + $Result = [System.Text.StringBuilder]::new("**$($Issues.Count) user permission$(if ($Issues.Count -eq 1) { '' } else { 's' })** allow unrestricted self-service creation.`n`n") + $null = $Result.Append("| Permission | Status |`n") + $null = $Result.Append("|------------|--------|`n") foreach ($Issue in $Issues) { - $Result += "| $Issue | ⚠️ Unrestricted |`n" + $null = $Result.Append("| $Issue | ⚠️ Unrestricted |`n") } foreach ($Ok in $Restricted) { - $Result += "| $Ok | ✅ Restricted |`n" + $null = $Result.Append("| $Ok | ✅ Restricted |`n") } - $Result += "`nWith Copilot deployed, unrestricted group and app creation increases the risk of uncontrolled data exposure. " - $Result += 'Restrict these permissions via **Entra ID → User settings** and **Group settings** to ensure new resources go through a governed process.' + $null = $Result.Append("`nWith Copilot deployed, unrestricted group and app creation increases the risk of uncontrolled data exposure. ") + $null = $Result.Append('Restrict these permissions via **Entra ID → User settings** and **Group settings** to ensure new resources go through a governed process.') } Add-CippTestResult -TenantFilter $Tenant -TestId 'CopilotReady012' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'User self-service creation is restricted (groups, tenants, apps)' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Copilot Readiness' diff --git a/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady013.ps1 b/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady013.ps1 index 95ccd53d9e2f..e72e1dfa6618 100644 --- a/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady013.ps1 +++ b/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady013.ps1 @@ -22,20 +22,20 @@ function Invoke-CippTestCopilotReady013 { if ($ActiveLabels.Count -gt 0) { $Status = 'Passed' - $Result = "**$($ActiveLabels.Count) active sensitivity label$(if ($ActiveLabels.Count -eq 1) { '' } else { 's' })** found in the tenant.`n`n" - $Result += "| Label | Parent | Has Protection |`n" - $Result += "|-------|--------|---------------|`n" + $Result = [System.Text.StringBuilder]::new("**$($ActiveLabels.Count) active sensitivity label$(if ($ActiveLabels.Count -eq 1) { '' } else { 's' })** found in the tenant.`n`n") + $null = $Result.Append("| Label | Parent | Has Protection |`n") + $null = $Result.Append("|-------|--------|---------------|`n") foreach ($Label in ($ActiveLabels | Sort-Object sensitivity)) { $ParentName = if ($Label.parent -and $Label.parent.name) { $Label.parent.name } else { '—' } $HasProtection = if ($Label.hasProtection -eq $true) { '✅ Yes' } else { 'No' } - $Result += "| $($Label.name) | $ParentName | $HasProtection |`n" + $null = $Result.Append("| $($Label.name) | $ParentName | $HasProtection |`n") } - $Result += "`nCopilot can respect and apply these labels when creating or summarizing content." + $null = $Result.Append("`nCopilot can respect and apply these labels when creating or summarizing content.") } else { $Status = 'Failed' - $Result = "No active sensitivity labels were found in this tenant.`n`n" - $Result += 'Sensitivity labels classify and protect organizational data — helping ensure Copilot-generated content is appropriately marked. ' - $Result += 'Configure labels in the [Microsoft Purview compliance portal](https://compliance.microsoft.com/informationprotection) before deploying Copilot.' + $Result = [System.Text.StringBuilder]::new("No active sensitivity labels were found in this tenant.`n`n") + $null = $Result.Append('Sensitivity labels classify and protect organizational data — helping ensure Copilot-generated content is appropriately marked. ') + $null = $Result.Append('Configure labels in the [Microsoft Purview compliance portal](https://compliance.microsoft.com/informationprotection) before deploying Copilot.') } Add-CippTestResult -TenantFilter $Tenant -TestId 'CopilotReady013' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Tenant has sensitivity labels configured in Purview' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Copilot Readiness' diff --git a/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady014.ps1 b/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady014.ps1 index d57a807a3747..0d5160c1a985 100644 --- a/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady014.ps1 +++ b/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady014.ps1 @@ -23,22 +23,22 @@ function Invoke-CippTestCopilotReady014 { if ($EnabledPolicies.Count -gt 0) { $Status = 'Passed' - $Result = "**$($EnabledPolicies.Count) enabled DLP polic$(if ($EnabledPolicies.Count -eq 1) { 'y' } else { 'ies' })** found in the tenant.`n`n" - $Result += "| Policy | Workload | Enabled |`n" - $Result += "|--------|----------|---------|`n" + $Result = [System.Text.StringBuilder]::new("**$($EnabledPolicies.Count) enabled DLP polic$(if ($EnabledPolicies.Count -eq 1) { 'y' } else { 'ies' })** found in the tenant.`n`n") + $null = $Result.Append("| Policy | Workload | Enabled |`n") + $null = $Result.Append("|--------|----------|---------|`n") foreach ($Policy in ($AllPolicies | Sort-Object DisplayName)) { $IsEnabled = if ($Policy.Mode -eq 'Enable' -and $Policy.Enabled -eq $true) { '✅ Yes' } else { 'No' } $Workload = if ($Policy.Workload) { $Policy.Workload } else { '—' } - $Result += "| $($Policy.DisplayName) | $Workload | $IsEnabled |`n" + $null = $Result.Append("| $($Policy.DisplayName) | $Workload | $IsEnabled |`n") } } else { $Status = 'Failed' - $Result = "No enabled DLP policies were found in this tenant.`n`n" + $Result = [System.Text.StringBuilder]::new("No enabled DLP policies were found in this tenant.`n`n") if ($AllPolicies.Count -gt 0) { - $Result += "**$($AllPolicies.Count) polic$(if ($AllPolicies.Count -eq 1) { 'y exists' } else { 'ies exist' })** but none are enabled.`n`n" + $null = $Result.Append("**$($AllPolicies.Count) polic$(if ($AllPolicies.Count -eq 1) { 'y exists' } else { 'ies exist' })** but none are enabled.`n`n") } - $Result += 'Data Loss Prevention policies help protect sensitive information from being shared inappropriately — a critical control when Copilot can surface content broadly. ' - $Result += 'Enable or create DLP policies in the [Microsoft Purview compliance portal](https://compliance.microsoft.com/datalossprevention) before deploying Copilot.' + $null = $Result.Append('Data Loss Prevention policies help protect sensitive information from being shared inappropriately — a critical control when Copilot can surface content broadly. ') + $null = $Result.Append('Enable or create DLP policies in the [Microsoft Purview compliance portal](https://compliance.microsoft.com/datalossprevention) before deploying Copilot.') } Add-CippTestResult -TenantFilter $Tenant -TestId 'CopilotReady014' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Tenant has enabled DLP policies configured' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Copilot Readiness' diff --git a/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady015.ps1 b/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady015.ps1 index d59731ab2114..3bb45ac64ef9 100644 --- a/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady015.ps1 +++ b/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady015.ps1 @@ -43,10 +43,10 @@ function Invoke-CippTestCopilotReady015 { $DisplayUsers = $ActiveUsers | Sort-Object lastActivityDate -Descending | Select-Object -First 50 foreach ($User in $DisplayUsers) { $LastActive = if ($User.lastActivityDate) { $User.lastActivityDate } else { 'N/A' } - $Row = "| $($User.userPrincipalName) | $LastActive |" + $Row = [System.Text.StringBuilder]::new("| $($User.userPrincipalName) | $LastActive |") foreach ($Col in $AppColumns) { $Val = $User.$Col - $Row += " $Val |" + $null = $Row.Append(" $Val |") } $Result += "$Row`n" } diff --git a/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady016.ps1 b/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady016.ps1 index 9ee4f3779981..5fca7f31fb2a 100644 --- a/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady016.ps1 +++ b/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady016.ps1 @@ -30,25 +30,25 @@ function Invoke-CippTestCopilotReady016 { $TotalAppCount = ($AppCounts | Measure-Object -Property Value -Sum).Sum ?? 0 if (-not $AppCounts -or $TotalAppCount -eq 0) { - $Result = "No Microsoft 365 Copilot usage was detected in the past 30 days.`n`n" - $Result += 'This tenant either has no Copilot licenses assigned or users have not yet started using Copilot features.' + $Result = [System.Text.StringBuilder]::new("No Microsoft 365 Copilot usage was detected in the past 30 days.`n`n") + $null = $Result.Append('This tenant either has no Copilot licenses assigned or users have not yet started using Copilot features.') Add-CippTestResult -TenantFilter $Tenant -TestId 'CopilotReady016' -TestType 'Identity' -Status 'Informational' -ResultMarkdown $Result -Risk 'Informational' -Name 'Copilot active user count by app' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Copilot Readiness' return } - $Result = "## Copilot Active Users by App (Last 30 Days)`n`n" - $Result += "| App | Active Users |`n" - $Result += "|-----|-------------|`n" + $Result = [System.Text.StringBuilder]::new("## Copilot Active Users by App (Last 30 Days)`n`n") + $null = $Result.Append("| App | Active Users |`n") + $null = $Result.Append("|-----|-------------|`n") foreach ($App in ($AppCounts | Sort-Object Value -Descending)) { # Format the property name to be more readable — insert space before each capital # that follows a lowercase letter to avoid double-spacing sequences like 'AI' -> 'A I' $AppName = $App.Name -replace '([a-z])([A-Z])', '$1 $2' -replace 'Active Users', '' - $Result += "| $($AppName.Trim()) | $($App.Value) |`n" + $null = $Result.Append("| $($AppName.Trim()) | $($App.Value) |`n") } if ($Summary.reportRefreshDate) { - $Result += "`n*Data as of $($Summary.reportRefreshDate).*" + $null = $Result.Append("`n*Data as of $($Summary.reportRefreshDate).*") } Add-CippTestResult -TenantFilter $Tenant -TestId 'CopilotReady016' -TestType 'Identity' -Status 'Informational' -ResultMarkdown $Result -Risk 'Informational' -Name 'Copilot active user count by app' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Copilot Readiness' diff --git a/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady017.ps1 b/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady017.ps1 index 374696adc407..10b13fb99ed6 100644 --- a/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady017.ps1 +++ b/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady017.ps1 @@ -21,8 +21,8 @@ function Invoke-CippTestCopilotReady017 { $TrendPoints = @($TrendData | Where-Object { $_.reportDate } | Sort-Object reportDate) if ($TrendPoints.Count -eq 0) { - $Result = "No Microsoft 365 Copilot usage trend data was found for the past 7 days.`n`n" - $Result += 'This tenant either has no Copilot licenses assigned or users have not yet started using Copilot features.' + $Result = [System.Text.StringBuilder]::new("No Microsoft 365 Copilot usage trend data was found for the past 7 days.`n`n") + $null = $Result.Append('This tenant either has no Copilot licenses assigned or users have not yet started using Copilot features.') Add-CippTestResult -TenantFilter $Tenant -TestId 'CopilotReady017' -TestType 'Identity' -Status 'Informational' -ResultMarkdown $Result -Risk 'Informational' -Name 'Copilot adoption trend (7-day)' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Copilot Readiness' return } @@ -33,13 +33,13 @@ function Invoke-CippTestCopilotReady017 { } | Select-Object -First 1 # Build trend table - $Result = "## Copilot Active User Trend (Last 7 Days)`n`n" - $Result += "| Date | Active Users |`n" - $Result += "|------|-------------|`n" + $Result = [System.Text.StringBuilder]::new("## Copilot Active User Trend (Last 7 Days)`n`n") + $null = $Result.Append("| Date | Active Users |`n") + $null = $Result.Append("|------|-------------|`n") foreach ($Point in $TrendPoints) { $Count = if ($CountField) { $Point.$CountField } else { 'N/A' } - $Result += "| $($Point.reportDate) | $Count |`n" + $null = $Result.Append("| $($Point.reportDate) | $Count |`n") } # Determine trend direction if we have a count field and at least 2 data points @@ -60,7 +60,7 @@ function Invoke-CippTestCopilotReady017 { $TrendText = "**Trending down** — active Copilot users decreased by $([math]::Abs($Delta)) over the 7-day window. Consider reviewing adoption activities to re-engage users." } - $Result += "`n$TrendIcon $TrendText" + $null = $Result.Append("`n$TrendIcon $TrendText") } Add-CippTestResult -TenantFilter $Tenant -TestId 'CopilotReady017' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Informational' -Name 'Copilot adoption trend (7-day)' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Copilot Readiness' diff --git a/Modules/CIPPTests/Public/Tests/Custom/Invoke-CippTestCustomScripts.ps1 b/Modules/CIPPTests/Public/Tests/Custom/Invoke-CippTestCustomScripts.ps1 index c9ab3230c8c3..c64a9b1d03ce 100644 --- a/Modules/CIPPTests/Public/Tests/Custom/Invoke-CippTestCustomScripts.ps1 +++ b/Modules/CIPPTests/Public/Tests/Custom/Invoke-CippTestCustomScripts.ps1 @@ -16,9 +16,19 @@ function Invoke-CippTestCustomScripts { return } - $LatestScripts = $Scripts | Group-Object -Property ScriptGuid | ForEach-Object { - $_.Group | Sort-Object -Property Version -Descending | Select-Object -First 1 + # Pick the latest version per ScriptGuid in a single pass instead of the + # original Group-Object | ForEach-Object { Sort-Object | Select -First 1 } + # pipeline (item 8). + $LatestByGuid = @{} + foreach ($S in $Scripts) { + $Guid = $S.ScriptGuid + if (-not $Guid) { continue } + $Existing = $LatestByGuid[$Guid] + if (-not $Existing -or [int]$S.Version -gt [int]$Existing.Version) { + $LatestByGuid[$Guid] = $S + } } + $LatestScripts = @($LatestByGuid.Values) if (-not [string]::IsNullOrWhiteSpace($ScriptGuid) -and $LatestScripts.Count -eq 0) { Write-Information "No latest custom script found for ScriptGuid: $ScriptGuid" @@ -26,25 +36,29 @@ function Invoke-CippTestCustomScripts { } foreach ($Script in $LatestScripts) { + # Cache PSObject property lookups once per script so we don't pay the + # member-resolution cost repeatedly inside the hot loop (item 13). + $Props = $Script.PSObject.Properties + $EnabledProp = $Props['Enabled'] + $AlertProp = $Props['AlertOnFailure'] + $ResultModeProp = $Props['ResultMode'] + $AlertStatusesProp = $Props['AlertStatuses'] + # We can't prefilter this on table lookup as each script version has its own Enabled property, so we need to check here if the latest version is enabled - $IsEnabled = if ($Script.PSObject.Properties['Enabled']) { [bool]$Script.Enabled } else { $true } + $IsEnabled = if ($EnabledProp) { [bool]$EnabledProp.Value } else { $true } if (-not $IsEnabled) { continue } - $ShouldAlert = $false - if ($Script.PSObject.Properties['AlertOnFailure']) { - $ShouldAlert = [bool]$Script.AlertOnFailure - } + $ShouldAlert = if ($AlertProp) { [bool]$AlertProp.Value } else { $false } - $ResultMode = if ($Script.PSObject.Properties['ResultMode'] -and -not [string]::IsNullOrWhiteSpace($Script.ResultMode)) { $Script.ResultMode } else { 'Auto' } + $ResultMode = if ($ResultModeProp -and -not [string]::IsNullOrWhiteSpace($ResultModeProp.Value)) { $ResultModeProp.Value } else { 'Auto' } $TestId = "CustomScript-$($Script.ScriptGuid)" $ScriptName = if ([string]::IsNullOrWhiteSpace($Script.ScriptName)) { $TestId } else { $Script.ScriptName } $AlertStatuses = @('Failed') - if ($Script.PSObject.Properties['AlertStatuses'] -and - -not [string]::IsNullOrWhiteSpace($Script.AlertStatuses)) { - $AlertStatuses = $Script.AlertStatuses | ConvertFrom-Json + if ($AlertStatusesProp -and -not [string]::IsNullOrWhiteSpace($AlertStatusesProp.Value)) { + $AlertStatuses = $AlertStatusesProp.Value | ConvertFrom-Json } try { diff --git a/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest001.ps1 b/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest001.ps1 index 2ae2479ef32b..3569629c5dd1 100644 --- a/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest001.ps1 +++ b/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest001.ps1 @@ -18,10 +18,10 @@ function Invoke-CippTestGenericTest001 { $TotalUsed = ($Licenses | ForEach-Object { [int]$_.CountUsed } | Measure-Object -Sum).Sum $OverallUtilization = if ($TotalLicenses -gt 0) { [math]::Round(($TotalUsed / $TotalLicenses) * 100, 1) } else { 0 } - $Result = "**Total Licenses:** $TotalLicenses | **In Use:** $TotalUsed | **Overall Utilization:** $OverallUtilization%`n`n" + $Result = [System.Text.StringBuilder]::new("**Total Licenses:** $TotalLicenses | **In Use:** $TotalUsed | **Overall Utilization:** $OverallUtilization%`n`n") - $Result += "| License | In Use | Total | Available | Utilization |`n" - $Result += "|---------|--------|-------|-----------|-------------|`n" + $null = $Result.Append("| License | In Use | Total | Available | Utilization |`n") + $null = $Result.Append("|---------|--------|-------|-----------|-------------|`n") foreach ($License in ($Licenses | Sort-Object { [int]$_.TotalLicenses } -Descending)) { $LicName = $License.License @@ -30,7 +30,7 @@ function Invoke-CippTestGenericTest001 { $Available = $Total - $Used $Util = if ($Total -gt 0) { [math]::Round(($Used / $Total) * 100, 0) } else { 0 } $UtilIcon = if ($Util -ge 90) { "🟢 $Util%" } elseif ($Util -ge 70) { "🟡 $Util%" } else { "🟢 $Util%" } - $Result += "| $LicName | $Used | $Total | $Available | $UtilIcon |`n" + $null = $Result.Append("| $LicName | $Used | $Total | $Available | $UtilIcon |`n") } diff --git a/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest002.ps1 b/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest002.ps1 index ffeb3fab4d4a..77314747c732 100644 --- a/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest002.ps1 +++ b/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest002.ps1 @@ -37,28 +37,28 @@ function Invoke-CippTestGenericTest002 { } if ($UserLicenseMap.Count -eq 0) { - $Result = "No users with assigned licenses were found in the cached data.`n`nThis may indicate that the license data has not been synced recently, or no licenses have been assigned to individual users." + $Result = [System.Text.StringBuilder]::new("No users with assigned licenses were found in the cached data.`n`nThis may indicate that the license data has not been synced recently, or no licenses have been assigned to individual users.") Add-CippTestResult -TenantFilter $Tenant -TestId 'GenericTest002' -TestType 'Identity' -Status 'Informational' -ResultMarkdown $Result -Risk 'Informational' -Name 'User License Overview' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Tenant Overview' return } - $Result = "**Total Licensed Users:** $($UserLicenseMap.Count)`n`n" + $Result = [System.Text.StringBuilder]::new("**Total Licensed Users:** $($UserLicenseMap.Count)`n`n") - $Result += "| User | Licenses |`n" - $Result += "|------|----------|`n" + $null = $Result.Append("| User | Licenses |`n") + $null = $Result.Append("|------|----------|`n") $SortedUsers = $UserLicenseMap.GetEnumerator() | Sort-Object { $_.Value.DisplayName } $DisplayCount = 0 foreach ($Entry in $SortedUsers) { $DisplayName = $Entry.Value.DisplayName $LicList = ($Entry.Value.Licenses | Sort-Object) -join ', ' - $Result += "| $DisplayName | $LicList |`n" + $null = $Result.Append("| $DisplayName | $LicList |`n") $DisplayCount++ if ($DisplayCount -ge 100) { break } } if ($UserLicenseMap.Count -gt 100) { - $Result += "`n*Showing 100 of $($UserLicenseMap.Count) licensed users.*`n" + $null = $Result.Append("`n*Showing 100 of $($UserLicenseMap.Count) licensed users.*`n") } Add-CippTestResult -TenantFilter $Tenant -TestId 'GenericTest002' -TestType 'Identity' -Status 'Informational' -ResultMarkdown $Result -Risk 'Informational' -Name 'User License Overview' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Tenant Overview' diff --git a/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest003.ps1 b/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest003.ps1 index e6dfc31ec0ce..bc6de5e0d904 100644 --- a/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest003.ps1 +++ b/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest003.ps1 @@ -15,7 +15,7 @@ function Invoke-CippTestGenericTest003 { $Licenses = @($LicenseData) - $Result = "" + $Result = [System.Text.StringBuilder]::new() $HasRenewals = $false $TrialCount = 0 @@ -44,24 +44,24 @@ function Invoke-CippTestGenericTest003 { } if (-not $HasRenewals) { - $Result += 'No subscription renewal information is available. This may indicate non-standard licensing or the data has not been synced recently.' + $null = $Result.Append('No subscription renewal information is available. This may indicate non-standard licensing or the data has not been synced recently.') Add-CippTestResult -TenantFilter $Tenant -TestId 'GenericTest003' -TestType 'Identity' -Status 'Informational' -ResultMarkdown $Result -Risk 'Informational' -Name 'License Renewal Report' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Tenant Overview' return } if ($TrialCount -gt 0) { - $Result += "**⚠️ $TrialCount trial subscription(s) detected.** Trial licenses will expire and may cause users to lose access if not converted to paid subscriptions.`n`n" + $null = $Result.Append("**⚠️ $TrialCount trial subscription(s) detected.** Trial licenses will expire and may cause users to lose access if not converted to paid subscriptions.`n`n") } $UrgentRenewals = @($UpcomingRenewals | Where-Object { $_.DaysUntilRenew -le 30 -and $_.DaysUntilRenew -ge 0 }) if ($UrgentRenewals.Count -gt 0) { - $Result += "**🔴 $($UrgentRenewals.Count) subscription(s) renewing within 30 days** — review these to ensure billing and seat counts are correct.`n`n" + $null = $Result.Append("**🔴 $($UrgentRenewals.Count) subscription(s) renewing within 30 days** — review these to ensure billing and seat counts are correct.`n`n") } $Sorted = $UpcomingRenewals | Sort-Object DaysUntilRenew - $Result += "| License | Status | Billing Term | Seats | Renews In | Renewal Date | Trial |`n" - $Result += "|---------|--------|--------------|-------|-----------|--------------|-------|`n" + $null = $Result.Append("| License | Status | Billing Term | Seats | Renews In | Renewal Date | Trial |`n") + $null = $Result.Append("|---------|--------|--------------|-------|-----------|--------------|-------|`n") foreach ($Renewal in $Sorted) { $DaysLabel = if ($null -eq $Renewal.DaysUntilRenew) { 'Unknown' } @@ -76,7 +76,7 @@ function Invoke-CippTestGenericTest003 { 'Deleted' { "❌ $($Renewal.Status)" } default { $Renewal.Status } } - $Result += "| $($Renewal.License) | $StatusIcon | $($Renewal.Term) | $($Renewal.Seats) | $DaysLabel | $($Renewal.NextRenewal) | $TrialLabel |`n" + $null = $Result.Append("| $($Renewal.License) | $StatusIcon | $($Renewal.Term) | $($Renewal.Seats) | $DaysLabel | $($Renewal.NextRenewal) | $TrialLabel |`n") } Add-CippTestResult -TenantFilter $Tenant -TestId 'GenericTest003' -TestType 'Identity' -Status 'Informational' -ResultMarkdown $Result -Risk 'Informational' -Name 'License Renewal Report' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Tenant Overview' diff --git a/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest004.ps1 b/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest004.ps1 index 47bf6671a71d..4d6b19eaeee2 100644 --- a/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest004.ps1 +++ b/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest004.ps1 @@ -29,25 +29,25 @@ function Invoke-CippTestGenericTest004 { $AdminCount = @($Users | Where-Object { $_.IsAdmin -eq $true }).Count $MFARegPct = if ($TotalUsers -gt 0) { [math]::Round(($MFARegistered / $TotalUsers) * 100, 1) } else { 0 } - $Result = "### Summary`n`n" - $Result += "| Metric | Count |`n" - $Result += "|--------|-------|`n" - $Result += "| Total Accounts | $TotalUsers |`n" - $Result += "| Admin Accounts | $AdminCount |`n" - $Result += "| Registered for MFA | $MFARegistered ($MFARegPct%) |`n" - $Result += "| MFA Capable | $MFACapable |`n" - $Result += "| Protected by Conditional Access | $CoveredByCA |`n" - $Result += "| Protected by Security Defaults | $CoveredBySD |`n" - $Result += "| Using Per-User MFA (Legacy) | $PerUserMFA |`n" - $Result += "| **Not Protected by Any MFA Policy** | **$NotProtected** |`n`n" + $Result = [System.Text.StringBuilder]::new("### Summary`n`n") + $null = $Result.Append("| Metric | Count |`n") + $null = $Result.Append("|--------|-------|`n") + $null = $Result.Append("| Total Accounts | $TotalUsers |`n") + $null = $Result.Append("| Admin Accounts | $AdminCount |`n") + $null = $Result.Append("| Registered for MFA | $MFARegistered ($MFARegPct%) |`n") + $null = $Result.Append("| MFA Capable | $MFACapable |`n") + $null = $Result.Append("| Protected by Conditional Access | $CoveredByCA |`n") + $null = $Result.Append("| Protected by Security Defaults | $CoveredBySD |`n") + $null = $Result.Append("| Using Per-User MFA (Legacy) | $PerUserMFA |`n") + $null = $Result.Append("| **Not Protected by Any MFA Policy** | **$NotProtected** |`n`n") if ($NotProtected -gt 0) { - $Result += "**⚠️ $NotProtected account(s) have no MFA enforcement.** These accounts are at significantly higher risk of compromise. Consider enabling Conditional Access policies to require MFA for all users.`n`n" + $null = $Result.Append("**⚠️ $NotProtected account(s) have no MFA enforcement.** These accounts are at significantly higher risk of compromise. Consider enabling Conditional Access policies to require MFA for all users.`n`n") } - $Result += "### All Accounts`n`n" - $Result += "| Display Name | MFA Registered | MFA Method | Protected By | Account Type |`n" - $Result += "|-------------|----------------|------------|--------------|--------------|`n" + $null = $Result.Append("### All Accounts`n`n") + $null = $Result.Append("| Display Name | MFA Registered | MFA Method | Protected By | Account Type |`n") + $null = $Result.Append("|-------------|----------------|------------|--------------|--------------|`n") $DisplayUsers = $Users | Sort-Object DisplayName | Select-Object -First 100 foreach ($User in $DisplayUsers) { @@ -64,11 +64,11 @@ function Invoke-CippTestGenericTest004 { elseif ($User.PerUser -in @('Enforced', 'Enabled')) { "Per-User MFA ($($User.PerUser))" } else { '❌ None' } $AcctType = if ($User.IsAdmin -eq $true) { '🔑 Admin' } else { 'User' } - $Result += "| $Name | $Registered | $Methods | $Protection | $AcctType |`n" + $null = $Result.Append("| $Name | $Registered | $Methods | $Protection | $AcctType |`n") } if ($Users.Count -gt 100) { - $Result += "`n*Showing 100 of $($Users.Count) accounts.*`n" + $null = $Result.Append("`n*Showing 100 of $($Users.Count) accounts.*`n") } Add-CippTestResult -TenantFilter $Tenant -TestId 'GenericTest004' -TestType 'Identity' -Status 'Informational' -ResultMarkdown $Result -Risk 'Informational' -Name 'Tenant MFA Report' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Tenant Overview' diff --git a/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest005.ps1 b/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest005.ps1 index 9e4e6943ba34..6f25700f1eba 100644 --- a/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest005.ps1 +++ b/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest005.ps1 @@ -16,7 +16,7 @@ function Invoke-CippTestGenericTest005 { $Admins = @($MFAData | Where-Object { $_.UPN -and $_.IsAdmin -eq $true }) if ($Admins.Count -eq 0) { - $Result = "No administrator accounts were found in the MFA state data. This is unusual and may indicate the data needs to be re-synced." + $Result = [System.Text.StringBuilder]::new("No administrator accounts were found in the MFA state data. This is unusual and may indicate the data needs to be re-synced.") Add-CippTestResult -TenantFilter $Tenant -TestId 'GenericTest005' -TestType 'Identity' -Status 'Informational' -ResultMarkdown $Result -Risk 'Informational' -Name 'Admin MFA Report' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Tenant Overview' return } @@ -26,20 +26,20 @@ function Invoke-CippTestGenericTest005 { $NotProtected = @($Admins | Where-Object { $_.CoveredByCA -notlike 'Enforced*' -and $_.CoveredBySD -ne $true -and $_.PerUser -notin @('Enforced', 'Enabled') }).Count $MFARegPct = if ($TotalAdmins -gt 0) { [math]::Round(($MFARegistered / $TotalAdmins) * 100, 1) } else { 0 } - $Result = "**Total Admins:** $TotalAdmins | **MFA Registered:** $MFARegistered ($MFARegPct%)" + $Result = [System.Text.StringBuilder]::new("**Total Admins:** $TotalAdmins | **MFA Registered:** $MFARegistered ($MFARegPct%)") if ($NotProtected -gt 0) { - $Result += " | **⚠️ Unprotected: $NotProtected**" + $null = $Result.Append(" | **⚠️ Unprotected: $NotProtected**") } - $Result += "`n`n" + $null = $Result.Append("`n`n") if ($NotProtected -gt 0) { - $Result += "**🔴 Critical: $NotProtected admin account(s) have no MFA enforcement.** Admin accounts without MFA are the #1 target for attackers. This should be addressed immediately.`n`n" + $null = $Result.Append("**🔴 Critical: $NotProtected admin account(s) have no MFA enforcement.** Admin accounts without MFA are the #1 target for attackers. This should be addressed immediately.`n`n") } elseif ($MFARegistered -eq $TotalAdmins) { - $Result += "**✅ All admin accounts have MFA registered and enforced.** Great job keeping your most privileged accounts secured.`n`n" + $null = $Result.Append("**✅ All admin accounts have MFA registered and enforced.** Great job keeping your most privileged accounts secured.`n`n") } - $Result += "| Display Name | MFA Registered | MFA Method | Protected By | Account Enabled |`n" - $Result += "|-------------|----------------|------------|--------------|-----------------|`n" + $null = $Result.Append("| Display Name | MFA Registered | MFA Method | Protected By | Account Enabled |`n") + $null = $Result.Append("|-------------|----------------|------------|--------------|-----------------|`n") foreach ($Admin in ($Admins | Sort-Object DisplayName)) { $Name = $Admin.DisplayName @@ -55,7 +55,7 @@ function Invoke-CippTestGenericTest005 { elseif ($Admin.PerUser -in @('Enforced', 'Enabled')) { "Per-User MFA ($($Admin.PerUser))" } else { '❌ None' } $Enabled = if ($Admin.AccountEnabled -eq $true) { 'Yes' } else { 'Disabled' } - $Result += "| $Name | $Registered | $Methods | $Protection | $Enabled |`n" + $null = $Result.Append("| $Name | $Registered | $Methods | $Protection | $Enabled |`n") } Add-CippTestResult -TenantFilter $Tenant -TestId 'GenericTest005' -TestType 'Identity' -Status 'Informational' -ResultMarkdown $Result -Risk 'Informational' -Name 'Admin MFA Report' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Tenant Overview' diff --git a/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest006.ps1 b/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest006.ps1 index 5c07c9c19a69..dee9fb7994df 100644 --- a/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest006.ps1 +++ b/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest006.ps1 @@ -16,7 +16,7 @@ function Invoke-CippTestGenericTest006 { $StandardUsers = @($MFAData | Where-Object { $_.UPN -and $_.IsAdmin -ne $true }) if ($StandardUsers.Count -eq 0) { - $Result = "No standard (non-admin) user accounts were found in the MFA state data." + $Result = [System.Text.StringBuilder]::new("No standard (non-admin) user accounts were found in the MFA state data.") Add-CippTestResult -TenantFilter $Tenant -TestId 'GenericTest006' -TestType 'Identity' -Status 'Informational' -ResultMarkdown $Result -Risk 'Informational' -Name 'User MFA Report' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Tenant Overview' return } @@ -26,18 +26,18 @@ function Invoke-CippTestGenericTest006 { $NotProtected = @($StandardUsers | Where-Object { $_.CoveredByCA -notlike 'Enforced*' -and $_.CoveredBySD -ne $true -and $_.PerUser -notin @('Enforced', 'Enabled') }).Count $MFARegPct = if ($TotalUsers -gt 0) { [math]::Round(($MFARegistered / $TotalUsers) * 100, 1) } else { 0 } - $Result = "**Total Users:** $TotalUsers | **MFA Registered:** $MFARegistered ($MFARegPct%)" + $Result = [System.Text.StringBuilder]::new("**Total Users:** $TotalUsers | **MFA Registered:** $MFARegistered ($MFARegPct%)") if ($NotProtected -gt 0) { - $Result += " | **Unprotected: $NotProtected**" + $null = $Result.Append(" | **Unprotected: $NotProtected**") } - $Result += "`n`n" + $null = $Result.Append("`n`n") if ($NotProtected -gt 0) { - $Result += "**⚠️ $NotProtected user account(s) have no MFA enforcement.** Consider enabling a Conditional Access policy that requires MFA for all users.`n`n" + $null = $Result.Append("**⚠️ $NotProtected user account(s) have no MFA enforcement.** Consider enabling a Conditional Access policy that requires MFA for all users.`n`n") } - $Result += "| Display Name | MFA Registered | MFA Method | Protected By | User Type |`n" - $Result += "|-------------|----------------|------------|--------------|-----------|`n" + $null = $Result.Append("| Display Name | MFA Registered | MFA Method | Protected By | User Type |`n") + $null = $Result.Append("|-------------|----------------|------------|--------------|-----------|`n") $DisplayUsers = $StandardUsers | Sort-Object DisplayName | Select-Object -First 100 foreach ($User in $DisplayUsers) { @@ -54,11 +54,11 @@ function Invoke-CippTestGenericTest006 { elseif ($User.PerUser -in @('Enforced', 'Enabled')) { "Per-User MFA ($($User.PerUser))" } else { '❌ None' } $UserType = if ($User.UserType -eq 'Guest') { 'Guest' } else { 'Member' } - $Result += "| $Name | $Registered | $Methods | $Protection | $UserType |`n" + $null = $Result.Append("| $Name | $Registered | $Methods | $Protection | $UserType |`n") } if ($StandardUsers.Count -gt 100) { - $Result += "`n*Showing 100 of $($StandardUsers.Count) user accounts.*`n" + $null = $Result.Append("`n*Showing 100 of $($StandardUsers.Count) user accounts.*`n") } Add-CippTestResult -TenantFilter $Tenant -TestId 'GenericTest006' -TestType 'Identity' -Status 'Informational' -ResultMarkdown $Result -Risk 'Informational' -Name 'User MFA Report' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Tenant Overview' diff --git a/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest007.ps1 b/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest007.ps1 index 15df3d07aa71..3d4d71b98fd3 100644 --- a/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest007.ps1 +++ b/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest007.ps1 @@ -16,7 +16,7 @@ function Invoke-CippTestGenericTest007 { $LicensedUsers = @($MFAData | Where-Object { $_.UPN -and $_.isLicensed -eq $true }) if ($LicensedUsers.Count -eq 0) { - $Result = "No licensed user accounts were found in the MFA state data. This may indicate no licenses have been assigned or the data needs to be re-synced." + $Result = [System.Text.StringBuilder]::new("No licensed user accounts were found in the MFA state data. This may indicate no licenses have been assigned or the data needs to be re-synced.") Add-CippTestResult -TenantFilter $Tenant -TestId 'GenericTest007' -TestType 'Identity' -Status 'Informational' -ResultMarkdown $Result -Risk 'Informational' -Name 'Licensed User MFA Report' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Tenant Overview' return } @@ -27,18 +27,18 @@ function Invoke-CippTestGenericTest007 { $Admins = @($LicensedUsers | Where-Object { $_.IsAdmin -eq $true }).Count $MFARegPct = if ($TotalUsers -gt 0) { [math]::Round(($MFARegistered / $TotalUsers) * 100, 1) } else { 0 } - $Result = "**Licensed Users:** $TotalUsers | **Admins among them:** $Admins | **MFA Registered:** $MFARegistered ($MFARegPct%)" + $Result = [System.Text.StringBuilder]::new("**Licensed Users:** $TotalUsers | **Admins among them:** $Admins | **MFA Registered:** $MFARegistered ($MFARegPct%)") if ($NotProtected -gt 0) { - $Result += " | **Unprotected: $NotProtected**" + $null = $Result.Append(" | **Unprotected: $NotProtected**") } - $Result += "`n`n" + $null = $Result.Append("`n`n") if ($NotProtected -gt 0) { - $Result += "**⚠️ $NotProtected licensed user(s) have no MFA enforcement.** These accounts have access to company data and email but are not protected by any MFA policy.`n`n" + $null = $Result.Append("**⚠️ $NotProtected licensed user(s) have no MFA enforcement.** These accounts have access to company data and email but are not protected by any MFA policy.`n`n") } - $Result += "| Display Name | Role | MFA Registered | MFA Method | Protected By |`n" - $Result += "|-------------|------|----------------|------------|--------------|`n" + $null = $Result.Append("| Display Name | Role | MFA Registered | MFA Method | Protected By |`n") + $null = $Result.Append("|-------------|------|----------------|------------|--------------|`n") $DisplayUsers = $LicensedUsers | Sort-Object DisplayName | Select-Object -First 100 foreach ($User in $DisplayUsers) { @@ -55,11 +55,11 @@ function Invoke-CippTestGenericTest007 { elseif ($User.CoveredBySD -eq $true) { 'Security Defaults' } elseif ($User.PerUser -in @('Enforced', 'Enabled')) { "Per-User MFA ($($User.PerUser))" } else { '❌ None' } - $Result += "| $Name | $Role | $Registered | $Methods | $Protection |`n" + $null = $Result.Append("| $Name | $Role | $Registered | $Methods | $Protection |`n") } if ($LicensedUsers.Count -gt 100) { - $Result += "`n*Showing 100 of $($LicensedUsers.Count) licensed user accounts.*`n" + $null = $Result.Append("`n*Showing 100 of $($LicensedUsers.Count) licensed user accounts.*`n") } Add-CippTestResult -TenantFilter $Tenant -TestId 'GenericTest007' -TestType 'Identity' -Status 'Informational' -ResultMarkdown $Result -Risk 'Informational' -Name 'Licensed User MFA Report' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Tenant Overview' diff --git a/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest008.ps1 b/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest008.ps1 index c4f401f8fc5f..f082405ac151 100644 --- a/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest008.ps1 +++ b/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest008.ps1 @@ -16,11 +16,11 @@ function Invoke-CippTestGenericTest008 { $AllUsers = @($MFAData | Where-Object { $_.UPN }) $PerUserMFAUsers = @($AllUsers | Where-Object { $_.PerUser -in @('Enforced', 'Enabled') }) - $Result = "" + $Result = [System.Text.StringBuilder]::new() if ($PerUserMFAUsers.Count -eq 0) { - $Result += "**✅ No accounts are using legacy Per-User MFA.** Your tenant is not relying on the deprecated per-user MFA enforcement method.`n`n" - $Result += "Make sure your accounts are protected by Conditional Access policies or Security Defaults instead." + $null = $Result.Append("**✅ No accounts are using legacy Per-User MFA.** Your tenant is not relying on the deprecated per-user MFA enforcement method.`n`n") + $null = $Result.Append("Make sure your accounts are protected by Conditional Access policies or Security Defaults instead.") Add-CippTestResult -TenantFilter $Tenant -TestId 'GenericTest008' -TestType 'Identity' -Status 'Informational' -ResultMarkdown $Result -Risk 'Informational' -Name 'Legacy Per-User MFA Report' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Tenant Overview' return } @@ -29,21 +29,21 @@ function Invoke-CippTestGenericTest008 { $EnabledCount = @($PerUserMFAUsers | Where-Object { $_.PerUser -eq 'Enabled' }).Count $AdminsAffected = @($PerUserMFAUsers | Where-Object { $_.IsAdmin -eq $true }).Count - $Result += "### Current Status`n`n" - $Result += "**⚠️ $($PerUserMFAUsers.Count) account(s) are still using legacy Per-User MFA.**`n`n" - $Result += "| Status | Count |`n" - $Result += "|--------|-------|`n" - $Result += "| Per-User MFA Enforced | $EnforcedCount |`n" - $Result += "| Per-User MFA Enabled | $EnabledCount |`n" + $null = $Result.Append("### Current Status`n`n") + $null = $Result.Append("**⚠️ $($PerUserMFAUsers.Count) account(s) are still using legacy Per-User MFA.**`n`n") + $null = $Result.Append("| Status | Count |`n") + $null = $Result.Append("|--------|-------|`n") + $null = $Result.Append("| Per-User MFA Enforced | $EnforcedCount |`n") + $null = $Result.Append("| Per-User MFA Enabled | $EnabledCount |`n") if ($AdminsAffected -gt 0) { - $Result += "| Admin Accounts Affected | $AdminsAffected |`n" + $null = $Result.Append("| Admin Accounts Affected | $AdminsAffected |`n") } - $Result += "`n" + $null = $Result.Append("`n") - $Result += "### Accounts Using Per-User MFA`n`n" - $Result += "The following accounts should be migrated to Conditional Access policies:`n`n" - $Result += "| Display Name | Per-User MFA Status | Also Covered by CA | Account Type | Licensed |`n" - $Result += "|-------------|--------------------|--------------------|--------------|----------|`n" + $null = $Result.Append("### Accounts Using Per-User MFA`n`n") + $null = $Result.Append("The following accounts should be migrated to Conditional Access policies:`n`n") + $null = $Result.Append("| Display Name | Per-User MFA Status | Also Covered by CA | Account Type | Licensed |`n") + $null = $Result.Append("|-------------|--------------------|--------------------|--------------|----------|`n") foreach ($User in ($PerUserMFAUsers | Sort-Object DisplayName)) { $Name = $User.DisplayName @@ -51,14 +51,14 @@ function Invoke-CippTestGenericTest008 { $CAProtected = if ($User.CoveredByCA -like 'Enforced*') { '✅ Yes' } else { '❌ No' } $AcctType = if ($User.IsAdmin -eq $true) { '🔑 Admin' } else { 'User' } $Licensed = if ($User.isLicensed -eq $true) { 'Yes' } else { 'No' } - $Result += "| $Name | $PerUserStatus | $CAProtected | $AcctType | $Licensed |`n" + $null = $Result.Append("| $Name | $PerUserStatus | $CAProtected | $AcctType | $Licensed |`n") } - $Result += "`n### Recommended Migration Steps`n`n" - $Result += "1. **Create a Conditional Access policy** that requires MFA for all users (or start with admins)`n" - $Result += "2. **Verify** the Conditional Access policy is working correctly for affected users`n" - $Result += "3. **Disable Per-User MFA** for each account listed above once confirmed`n" - $Result += "4. **Test sign-in** to confirm users can still authenticate properly`n" + $null = $Result.Append("`n### Recommended Migration Steps`n`n") + $null = $Result.Append("1. **Create a Conditional Access policy** that requires MFA for all users (or start with admins)`n") + $null = $Result.Append("2. **Verify** the Conditional Access policy is working correctly for affected users`n") + $null = $Result.Append("3. **Disable Per-User MFA** for each account listed above once confirmed`n") + $null = $Result.Append("4. **Test sign-in** to confirm users can still authenticate properly`n") Add-CippTestResult -TenantFilter $Tenant -TestId 'GenericTest008' -TestType 'Identity' -Status 'Informational' -ResultMarkdown $Result -Risk 'Informational' -Name 'Legacy Per-User MFA Report' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Tenant Overview' diff --git a/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest009.ps1 b/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest009.ps1 index f59f5be507c5..fc7054d88b82 100644 --- a/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest009.ps1 +++ b/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest009.ps1 @@ -32,31 +32,31 @@ function Invoke-CippTestGenericTest009 { $ScoreChange = [math]::Round($CurrentScore - $OldestScore, 1) $TrendIcon = if ($ScoreChange -gt 0) { "📈 +$ScoreChange" } elseif ($ScoreChange -lt 0) { "📉 $ScoreChange" } else { '➡️ No change' } - $Result = "### Current Score`n`n" - $Result += "| Metric | Value |`n" - $Result += "|--------|-------|`n" - $Result += "| Current Score | **$CurrentScore** out of $MaxScore ($ScorePct%) |`n" - $Result += "| 14-Day Trend | $TrendIcon |`n" - $Result += "| Data Points | $($SortedScores.Count) days |`n`n" + $Result = [System.Text.StringBuilder]::new("### Current Score`n`n") + $null = $Result.Append("| Metric | Value |`n") + $null = $Result.Append("|--------|-------|`n") + $null = $Result.Append("| Current Score | **$CurrentScore** out of $MaxScore ($ScorePct%) |`n") + $null = $Result.Append("| 14-Day Trend | $TrendIcon |`n") + $null = $Result.Append("| Data Points | $($SortedScores.Count) days |`n`n") if ($ScorePct -ge 80) { - $Result += "**✅ Strong security posture.** Your score is in the top tier. Keep monitoring to maintain this level.`n`n" + $null = $Result.Append("**✅ Strong security posture.** Your score is in the top tier. Keep monitoring to maintain this level.`n`n") } elseif ($ScorePct -ge 50) { - $Result += "**🟡 Moderate security posture.** There's room for improvement. Review the recommended actions in your Microsoft 365 Security portal.`n`n" + $null = $Result.Append("**🟡 Moderate security posture.** There's room for improvement. Review the recommended actions in your Microsoft 365 Security portal.`n`n") } else { - $Result += "**🔴 Low security posture.** Significant improvements are recommended. Focus on the high-impact actions first.`n`n" + $null = $Result.Append("**🔴 Low security posture.** Significant improvements are recommended. Focus on the high-impact actions first.`n`n") } - $Result += "### 14-Day Score Trend`n`n" - $Result += "| Date | Score | Max Score | Percentage |`n" - $Result += "|------|-------|-----------|------------|`n" + $null = $Result.Append("### 14-Day Score Trend`n`n") + $null = $Result.Append("| Date | Score | Max Score | Percentage |`n") + $null = $Result.Append("|------|-------|-----------|------------|`n") foreach ($Score in $SortedScores) { $DateStr = if ($Score.createdDateTime) { ([datetime]$Score.createdDateTime).ToString('yyyy-MM-dd') } else { 'Unknown' } $DayScore = [math]::Round([double]$Score.currentScore, 1) $DayMax = [math]::Round([double]$Score.maxScore, 1) $DayPct = if ($DayMax -gt 0) { [math]::Round(($DayScore / $DayMax) * 100, 1) } else { 0 } - $Result += "| $DateStr | $DayScore | $DayMax | $DayPct% |`n" + $null = $Result.Append("| $DateStr | $DayScore | $DayMax | $DayPct% |`n") } # Show top improvable controls if available @@ -79,12 +79,12 @@ function Invoke-CippTestGenericTest009 { } | Where-Object { $_.Gap -gt 0 } | Sort-Object Gap -Descending | Select-Object -First 10) if ($ImprovableControls.Count -gt 0) { - $Result += "`n### Top Improvement Opportunities`n`n" - $Result += "| Control | Current | Max | Points Available |`n" - $Result += "|---------|---------|-----|-----------------|`n" + $null = $Result.Append("`n### Top Improvement Opportunities`n`n") + $null = $Result.Append("| Control | Current | Max | Points Available |`n") + $null = $Result.Append("|---------|---------|-----|-----------------|`n") foreach ($Control in $ImprovableControls) { - $Result += "| $($Control.Name) | $($Control.Score) | $($Control.MaxScore) | +$($Control.Gap) |`n" + $null = $Result.Append("| $($Control.Name) | $($Control.Score) | $($Control.MaxScore) | +$($Control.Gap) |`n") } } } diff --git a/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest010.ps1 b/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest010.ps1 index 0fb523f95270..4191ac46f5f9 100644 --- a/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest010.ps1 +++ b/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest010.ps1 @@ -17,7 +17,7 @@ function Invoke-CippTestGenericTest010 { $CapabilityProperties = $Capabilities.PSObject.Properties | Where-Object { $_.Value -eq $true } if (-not $CapabilityProperties -or $CapabilityProperties.Count -eq 0) { - $Result = "No active service plans were found for this tenant. This is unusual and may indicate a licensing issue." + $Result = [System.Text.StringBuilder]::new("No active service plans were found for this tenant. This is unusual and may indicate a licensing issue.") Add-CippTestResult -TenantFilter $Tenant -TestId 'GenericTest010' -TestType 'Identity' -Status 'Informational' -ResultMarkdown $Result -Risk 'Informational' -Name 'Tenant Capabilities Report' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Tenant Overview' return } @@ -33,7 +33,7 @@ function Invoke-CippTestGenericTest010 { } } - $Result = "**Total Active Capabilities:** $($CapabilityProperties.Count)`n`n" + $Result = [System.Text.StringBuilder]::new("**Total Active Capabilities:** $($CapabilityProperties.Count)`n`n") # Categorize capabilities into logical groups $Categories = [ordered]@{ @@ -54,9 +54,9 @@ function Invoke-CippTestGenericTest010 { $CategoryPlans = @($AllPlanNames | Where-Object { $_ -in $Categories[$CategoryName] }) if ($CategoryPlans.Count -eq 0) { continue } - $Result += "### $CategoryName`n`n" - $Result += "| Capability | Service Plan |`n" - $Result += "|------------|-------------|`n" + $null = $Result.Append("### $CategoryName`n`n") + $null = $Result.Append("| Capability | Service Plan |`n") + $null = $Result.Append("|------------|-------------|`n") foreach ($Plan in ($CategoryPlans | Sort-Object)) { $FriendlyName = if ($FriendlyNameMap.ContainsKey($Plan)) { @@ -65,17 +65,17 @@ function Invoke-CippTestGenericTest010 { # Convert raw plan name to readable format $Plan -replace '_', ' ' -replace '([a-z])([A-Z])', '$1 $2' -replace ' S ', ' ' -replace ' O365 ', ' ' -replace ' P\d$', '' -replace ' ENTERPRISE', '' -replace ' PREMIUM', ' Premium' -replace ' STANDARD', '' } - $Result += "| $FriendlyName | $Plan |`n" + $null = $Result.Append("| $FriendlyName | $Plan |`n") } - $Result += "`n" + $null = $Result.Append("`n") } # Any uncategorized plans $Uncategorized = @($AllPlanNames | Where-Object { $_ -notin $CategorizedPlanNames }) if ($Uncategorized.Count -gt 0) { - $Result += "### Other Capabilities`n`n" - $Result += "| Capability | Service Plan |`n" - $Result += "|------------|-------------|`n" + $null = $Result.Append("### Other Capabilities`n`n") + $null = $Result.Append("| Capability | Service Plan |`n") + $null = $Result.Append("|------------|-------------|`n") foreach ($Plan in ($Uncategorized | Sort-Object)) { $FriendlyName = if ($FriendlyNameMap.ContainsKey($Plan)) { @@ -83,7 +83,7 @@ function Invoke-CippTestGenericTest010 { } else { $Plan -replace '_', ' ' -replace '([a-z])([A-Z])', '$1 $2' -replace ' S ', ' ' -replace ' O365 ', ' ' -replace ' P\d$', '' -replace ' ENTERPRISE', '' -replace ' PREMIUM', ' Premium' -replace ' STANDARD', '' } - $Result += "| $FriendlyName | $Plan |`n" + $null = $Result.Append("| $FriendlyName | $Plan |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest011.ps1 b/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest011.ps1 index f8b41220a0e9..ac3e9105eea4 100644 --- a/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest011.ps1 +++ b/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest011.ps1 @@ -51,7 +51,7 @@ function Invoke-CippTestGenericTest011 { } catch { $AllCATemplates = @() } $AlignmentItems = @($AlignmentData) - $Result = '' + $Result = [System.Text.StringBuilder]::new() # Helper: resolve a standard name to a friendly display name # Mirrors the resolution chain from Get-CIPPDrift.ps1 and the frontend drift.js @@ -139,19 +139,19 @@ function Invoke-CippTestGenericTest011 { $ScoreIcon = if ($Score -ge 80) { '✅' } elseif ($Score -ge 50) { '🟡' } else { '🔴' } - $Result += "### $TemplateName`n`n" - $Result += "**Alignment Score:** $ScoreIcon $Score% | **Compliant:** $Compliant / $Total" - if ($LicenseMissing -gt 0) { $Result += " | **License Missing:** $LicenseMissing" } - if ($ReportingDisabled -gt 0) { $Result += " | **Reporting Disabled:** $ReportingDisabled" } + $null = $Result.Append("### $TemplateName`n`n") + $null = $Result.Append("**Alignment Score:** $ScoreIcon $Score% | **Compliant:** $Compliant / $Total") + if ($LicenseMissing -gt 0) { $null = $Result.Append(" | **License Missing:** $LicenseMissing") } + if ($ReportingDisabled -gt 0) { $null = $Result.Append(" | **Reporting Disabled:** $ReportingDisabled") } if ($Template.LatestDataCollection) { $CollectionDate = ([datetime]$Template.LatestDataCollection).ToString('yyyy-MM-dd HH:mm') - $Result += " | **Last Checked:** $CollectionDate" + $null = $Result.Append(" | **Last Checked:** $CollectionDate") } - $Result += "`n`n" + $null = $Result.Append("`n`n") $Details = $Template.ComparisonDetails if (-not $Details) { - $Result += "No comparison details available for this template.`n`n" + $null = $Result.Append("No comparison details available for this template.`n`n") continue } @@ -166,53 +166,53 @@ function Invoke-CippTestGenericTest011 { # Compliant items if ($CompliantItems.Count -gt 0) { - $Result += "| Standard | Status |`n" - $Result += "|----------|--------|`n" + $null = $Result.Append("| Standard | Status |`n") + $null = $Result.Append("|----------|--------|`n") foreach ($Item in $CompliantItems) { $FriendlyName = & $ResolveDisplayName $Item.StandardName $TemplateSettings if (-not $FriendlyName) { continue } - $Result += "| $FriendlyName | ✅ Compliant |`n" + $null = $Result.Append("| $FriendlyName | ✅ Compliant |`n") } - $Result += "`n" + $null = $Result.Append("`n") } # Non-compliant items if ($NonCompliantItems.Count -gt 0) { - $Result += "| Standard | Status |`n" - $Result += "|----------|--------|`n" + $null = $Result.Append("| Standard | Status |`n") + $null = $Result.Append("|----------|--------|`n") foreach ($Item in $NonCompliantItems) { $FriendlyName = & $ResolveDisplayName $Item.StandardName $TemplateSettings if (-not $FriendlyName) { continue } - $Result += "| $FriendlyName | ❌ Non-Compliant |`n" + $null = $Result.Append("| $FriendlyName | ❌ Non-Compliant |`n") } - $Result += "`n" + $null = $Result.Append("`n") } # License missing items if ($LicenseMissingItems.Count -gt 0) { - $Result += "#### Standards Not Applied Due to Missing Licenses`n`n" - $Result += "These items are part of this baseline, but your environment does not meet the minimum required licenses for them to be applied.`n`n" - $Result += "| Standard | Status |`n" - $Result += "|----------|--------|`n" + $null = $Result.Append("#### Standards Not Applied Due to Missing Licenses`n`n") + $null = $Result.Append("These items are part of this baseline, but your environment does not meet the minimum required licenses for them to be applied.`n`n") + $null = $Result.Append("| Standard | Status |`n") + $null = $Result.Append("|----------|--------|`n") foreach ($Item in $LicenseMissingItems) { $FriendlyName = & $ResolveDisplayName $Item.StandardName $TemplateSettings if (-not $FriendlyName) { continue } - $Result += "| $FriendlyName | ⚠️ License Missing |`n" + $null = $Result.Append("| $FriendlyName | ⚠️ License Missing |`n") } - $Result += "`n" + $null = $Result.Append("`n") } # Reporting disabled items if ($ReportingDisabledItems.Count -gt 0) { - $Result += "#### Standards With Reporting Disabled`n`n" - $Result += "| Standard | Status |`n" - $Result += "|----------|--------|`n" + $null = $Result.Append("#### Standards With Reporting Disabled`n`n") + $null = $Result.Append("| Standard | Status |`n") + $null = $Result.Append("|----------|--------|`n") foreach ($Item in $ReportingDisabledItems) { $FriendlyName = & $ResolveDisplayName $Item.StandardName $TemplateSettings if (-not $FriendlyName) { continue } - $Result += "| $FriendlyName | ⏸️ Reporting Disabled |`n" + $null = $Result.Append("| $FriendlyName | ⏸️ Reporting Disabled |`n") } - $Result += "`n" + $null = $Result.Append("`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA100.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA100.ps1 index bc145b4e233e..9c62fa6ec28c 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA100.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA100.ps1 @@ -27,16 +27,16 @@ function Invoke-CippTestORCA100 { if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = "All anti-spam policies have appropriate Bulk Complaint Level (BCL) thresholds set between 4 and 6.`n`n" - $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + $Result = [System.Text.StringBuilder]::new("All anti-spam policies have appropriate Bulk Complaint Level (BCL) thresholds set between 4 and 6.`n`n") + $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)") } else { $Status = 'Failed' - $Result = "$($FailedPolicies.Count) anti-spam policies have BCL thresholds outside the recommended range (4-6).`n`n" - $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" - $Result += "| Policy Name | Current BCL Threshold |`n" - $Result += "|------------|----------------------|`n" + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) anti-spam policies have BCL thresholds outside the recommended range (4-6).`n`n") + $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") + $null = $Result.Append("| Policy Name | Current BCL Threshold |`n") + $null = $Result.Append("|------------|----------------------|`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.Identity) | $($Policy.BulkThreshold) |`n" + $null = $Result.Append("| $($Policy.Identity) | $($Policy.BulkThreshold) |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA101.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA101.ps1 index cd51a41a228e..0e9d3ec3b7a5 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA101.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA101.ps1 @@ -26,23 +26,23 @@ function Invoke-CippTestORCA101 { if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = "All anti-spam policies are configured to mark bulk mail as spam.`n`n" - $Result += "**Compliant Policies:** $($PassedPolicies.Count)`n`n" + $Result = [System.Text.StringBuilder]::new("All anti-spam policies are configured to mark bulk mail as spam.`n`n") + $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)`n`n") if ($PassedPolicies.Count -gt 0) { - $Result += "| Policy Name | Mark As Spam Bulk Mail |`n" - $Result += "|------------|------------------------|`n" + $null = $Result.Append("| Policy Name | Mark As Spam Bulk Mail |`n") + $null = $Result.Append("|------------|------------------------|`n") foreach ($Policy in $PassedPolicies) { - $Result += "| $($Policy.Identity) | $($Policy.MarkAsSpamBulkMail) |`n" + $null = $Result.Append("| $($Policy.Identity) | $($Policy.MarkAsSpamBulkMail) |`n") } } } else { $Status = 'Failed' - $Result = "$($FailedPolicies.Count) anti-spam policies are not configured to mark bulk mail as spam.`n`n" - $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" - $Result += "| Policy Name | Mark As Spam Bulk Mail |`n" - $Result += "|------------|------------------------|`n" + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) anti-spam policies are not configured to mark bulk mail as spam.`n`n") + $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") + $null = $Result.Append("| Policy Name | Mark As Spam Bulk Mail |`n") + $null = $Result.Append("|------------|------------------------|`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.Identity) | $($Policy.MarkAsSpamBulkMail) |`n" + $null = $Result.Append("| $($Policy.Identity) | $($Policy.MarkAsSpamBulkMail) |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA102.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA102.ps1 index 8d91be619f29..5d4459f46995 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA102.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA102.ps1 @@ -46,21 +46,21 @@ function Invoke-CippTestORCA102 { if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = "All anti-spam policies have Advanced Spam Filter (ASF) options turned off.`n`n" - $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + $Result = [System.Text.StringBuilder]::new("All anti-spam policies have Advanced Spam Filter (ASF) options turned off.`n`n") + $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)") } else { $Status = 'Failed' - $Result = "$($FailedPolicies.Count) anti-spam policies have Advanced Spam Filter (ASF) options enabled.`n`n" - $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" - $Result += "| Policy Name | Enabled ASF Options |`n" - $Result += "|------------|---------------------|`n" + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) anti-spam policies have Advanced Spam Filter (ASF) options enabled.`n`n") + $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") + $null = $Result.Append("| Policy Name | Enabled ASF Options |`n") + $null = $Result.Append("|------------|---------------------|`n") foreach ($Policy in $FailedPolicies) { $EnabledOptions = [System.Collections.Generic.List[string]]::new() if ($Policy.IncreaseScoreWithImageLinks -eq 'On') { $EnabledOptions.Add('ImageLinks') | Out-Null } if ($Policy.IncreaseScoreWithNumericIps -eq 'On') { $EnabledOptions.Add('NumericIPs') | Out-Null } if ($Policy.MarkAsSpamEmptyMessages -eq 'On') { $EnabledOptions.Add('EmptyMessages') | Out-Null } if ($Policy.MarkAsSpamJavaScriptInHtml -eq 'On') { $EnabledOptions.Add('JavaScript') | Out-Null } - $Result += "| $($Policy.Identity) | $($EnabledOptions -join ', ') |`n" + $null = $Result.Append("| $($Policy.Identity) | $($EnabledOptions -join ', ') |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA103.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA103.ps1 index 636048dbf752..8ee002e8f83e 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA103.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA103.ps1 @@ -45,16 +45,16 @@ function Invoke-CippTestORCA103 { if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = "All outbound spam filter policies are configured correctly.`n`n" - $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + $Result = [System.Text.StringBuilder]::new("All outbound spam filter policies are configured correctly.`n`n") + $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)") } else { $Status = 'Failed' - $Result = "$($FailedPolicies.Count) outbound spam filter policies are not configured correctly.`n`n" - $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" - $Result += "| Policy Name | Issues |`n" - $Result += "|------------|--------|`n" + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) outbound spam filter policies are not configured correctly.`n`n") + $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") + $null = $Result.Append("| Policy Name | Issues |`n") + $null = $Result.Append("|------------|--------|`n") foreach ($Failed in $FailedPolicies) { - $Result += "| $($Failed.Policy.Identity) | $($Failed.Issues -join '
') |`n" + $null = $Result.Append("| $($Failed.Policy.Identity) | $($Failed.Issues -join '
') |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA104.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA104.ps1 index 53aabdf3dbc5..da2cd06bfc06 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA104.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA104.ps1 @@ -27,26 +27,26 @@ function Invoke-CippTestORCA104 { if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = "All anti-phishing policies have High Confidence Phish action set to Quarantine.`n`n" - $Result += "**Compliant Policies:** $($PassedPolicies.Count)`n`n" + $Result = [System.Text.StringBuilder]::new("All anti-phishing policies have High Confidence Phish action set to Quarantine.`n`n") + $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)`n`n") if ($PassedPolicies.Count -gt 0) { - $Result += "| Policy Name | Action |`n" - $Result += "|------------|--------|`n" + $null = $Result.Append("| Policy Name | Action |`n") + $null = $Result.Append("|------------|--------|`n") foreach ($Policy in $PassedPolicies) { - $Result += "| $($Policy.Identity) | $($Policy.HighConfidencePhishAction) |`n" + $null = $Result.Append("| $($Policy.Identity) | $($Policy.HighConfidencePhishAction) |`n") } } } else { $Status = 'Failed' - $Result = "Some anti-phishing policies do not have High Confidence Phish action set to Quarantine.`n`n" - $Result += "**Failed Policies:** $($FailedPolicies.Count) | **Passed Policies:** $($PassedPolicies.Count)`n`n" - $Result += "### Non-Compliant Policies`n`n" - $Result += "| Policy Name | Current Action | Recommended Action |`n" - $Result += "|------------|----------------|-------------------|`n" + $Result = [System.Text.StringBuilder]::new("Some anti-phishing policies do not have High Confidence Phish action set to Quarantine.`n`n") + $null = $Result.Append("**Failed Policies:** $($FailedPolicies.Count) | **Passed Policies:** $($PassedPolicies.Count)`n`n") + $null = $Result.Append("### Non-Compliant Policies`n`n") + $null = $Result.Append("| Policy Name | Current Action | Recommended Action |`n") + $null = $Result.Append("|------------|----------------|-------------------|`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.Identity) | $($Policy.HighConfidencePhishAction) | Quarantine |`n" + $null = $Result.Append("| $($Policy.Identity) | $($Policy.HighConfidencePhishAction) | Quarantine |`n") } - $Result += "`n**Remediation:** Update the HighConfidencePhishAction to 'Quarantine' for enhanced security." + $null = $Result.Append("`n**Remediation:** Update the HighConfidencePhishAction to 'Quarantine' for enhanced security.") } Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA104' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'High Confidence Phish action set to Quarantine message' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Anti-Phish' diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA105.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA105.ps1 index 58c6a253b0f6..4a89546a6022 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA105.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA105.ps1 @@ -26,16 +26,16 @@ function Invoke-CippTestORCA105 { if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = "All Safe Links policies have synchronous URL detonation enabled.`n`n" - $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + $Result = [System.Text.StringBuilder]::new("All Safe Links policies have synchronous URL detonation enabled.`n`n") + $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)") } else { $Status = 'Failed' - $Result = "$($FailedPolicies.Count) Safe Links policies do not have synchronous URL detonation enabled.`n`n" - $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" - $Result += "| Policy Name | Deliver Message After Scan |`n" - $Result += "|------------|---------------------------|`n" + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) Safe Links policies do not have synchronous URL detonation enabled.`n`n") + $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") + $null = $Result.Append("| Policy Name | Deliver Message After Scan |`n") + $null = $Result.Append("|------------|---------------------------|`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.Identity) | $($Policy.DeliverMessageAfterScan) |`n" + $null = $Result.Append("| $($Policy.Identity) | $($Policy.DeliverMessageAfterScan) |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA106.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA106.ps1 index aca84b48492c..ae9d99104142 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA106.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA106.ps1 @@ -26,16 +26,16 @@ function Invoke-CippTestORCA106 { if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = "All anti-spam policies have quarantine retention period set to 30 days.`n`n" - $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + $Result = [System.Text.StringBuilder]::new("All anti-spam policies have quarantine retention period set to 30 days.`n`n") + $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)") } else { $Status = 'Failed' - $Result = "$($FailedPolicies.Count) anti-spam policies do not have quarantine retention period set to 30 days.`n`n" - $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" - $Result += "| Policy Name | Quarantine Retention Period |`n" - $Result += "|------------|----------------------------|`n" + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) anti-spam policies do not have quarantine retention period set to 30 days.`n`n") + $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") + $null = $Result.Append("| Policy Name | Quarantine Retention Period |`n") + $null = $Result.Append("|------------|----------------------------|`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.Identity) | $($Policy.QuarantineRetentionPeriod) days |`n" + $null = $Result.Append("| $($Policy.Identity) | $($Policy.QuarantineRetentionPeriod) days |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA107.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA107.ps1 index d7acf2e2f0b7..90655ffcf9c1 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA107.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA107.ps1 @@ -26,24 +26,24 @@ function Invoke-CippTestORCA107 { if ($FailedPolicies.Count -eq 0 -and $PassedPolicies.Count -gt 0) { $Status = 'Passed' - $Result = "All quarantine policies have end-user spam notifications enabled.`n`n" - $Result += "**Compliant Policies:** $($PassedPolicies.Count)`n`n" - $Result += "| Policy Name | Notification Frequency (days) |`n" - $Result += "|------------|-------------------------------|`n" + $Result = [System.Text.StringBuilder]::new("All quarantine policies have end-user spam notifications enabled.`n`n") + $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)`n`n") + $null = $Result.Append("| Policy Name | Notification Frequency (days) |`n") + $null = $Result.Append("|------------|-------------------------------|`n") foreach ($Policy in $PassedPolicies) { - $Result += "| $($Policy.Identity) | $($Policy.EndUserSpamNotificationFrequency) |`n" + $null = $Result.Append("| $($Policy.Identity) | $($Policy.EndUserSpamNotificationFrequency) |`n") } } elseif ($PassedPolicies.Count -eq 0) { $Status = 'Failed' - $Result = "No quarantine policies have end-user spam notifications enabled.`n`n" + $Result = [System.Text.StringBuilder]::new("No quarantine policies have end-user spam notifications enabled.`n`n") } else { $Status = 'Failed' - $Result = "$($FailedPolicies.Count) quarantine policies do not have end-user spam notifications enabled.`n`n" - $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" - $Result += "| Policy Name | Notification Frequency |`n" - $Result += "|------------|----------------------|`n" + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) quarantine policies do not have end-user spam notifications enabled.`n`n") + $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") + $null = $Result.Append("| Policy Name | Notification Frequency |`n") + $null = $Result.Append("|------------|----------------------|`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.Identity) | Disabled |`n" + $null = $Result.Append("| $($Policy.Identity) | Disabled |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA108_1.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA108_1.ps1 index 62abde9f0eb7..b447b27296cc 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA108_1.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA108_1.ps1 @@ -30,16 +30,16 @@ function Invoke-CippTestORCA108_1 { if ($FailedDomains.Count -eq 0) { $Status = 'Passed' - $Result = "All custom domains have DKIM DNS records configured.`n`n" - $Result += "**Compliant Domains:** $($PassedDomains.Count)" + $Result = [System.Text.StringBuilder]::new("All custom domains have DKIM DNS records configured.`n`n") + $null = $Result.Append("**Compliant Domains:** $($PassedDomains.Count)") } else { $Status = 'Failed' - $Result = "$($FailedDomains.Count) custom domains do not have DKIM DNS records configured.`n`n" - $Result += "**Non-Compliant Domains:** $($FailedDomains.Count)`n`n" - $Result += "| Domain Name |`n" - $Result += "|------------|`n" + $Result = [System.Text.StringBuilder]::new("$($FailedDomains.Count) custom domains do not have DKIM DNS records configured.`n`n") + $null = $Result.Append("**Non-Compliant Domains:** $($FailedDomains.Count)`n`n") + $null = $Result.Append("| Domain Name |`n") + $null = $Result.Append("|------------|`n") foreach ($Domain in $FailedDomains) { - $Result += "| $($Domain.DomainName) |`n" + $null = $Result.Append("| $($Domain.DomainName) |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA109.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA109.ps1 index a5446cece14c..20aed3932e3d 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA109.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA109.ps1 @@ -29,18 +29,18 @@ function Invoke-CippTestORCA109 { if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = "No anti-spam policies have sender allow lists configured.`n`n" - $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + $Result = [System.Text.StringBuilder]::new("No anti-spam policies have sender allow lists configured.`n`n") + $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)") } else { $Status = 'Failed' - $Result = "$($FailedPolicies.Count) anti-spam policies have sender allow lists configured.`n`n" - $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" - $Result += "| Policy Name | Allowed Senders | Allowed Sender Domains |`n" - $Result += "|------------|----------------|----------------------|`n" + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) anti-spam policies have sender allow lists configured.`n`n") + $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") + $null = $Result.Append("| Policy Name | Allowed Senders | Allowed Sender Domains |`n") + $null = $Result.Append("|------------|----------------|----------------------|`n") foreach ($Policy in $FailedPolicies) { $SenderCount = if ($Policy.AllowedSenders) { $Policy.AllowedSenders.Count } else { 0 } $DomainCount = if ($Policy.AllowedSenderDomains) { $Policy.AllowedSenderDomains.Count } else { 0 } - $Result += "| $($Policy.Identity) | $SenderCount | $DomainCount |`n" + $null = $Result.Append("| $($Policy.Identity) | $SenderCount | $DomainCount |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA110.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA110.ps1 index d37e046d43d2..2f3ed57eeffc 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA110.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA110.ps1 @@ -26,16 +26,16 @@ function Invoke-CippTestORCA110 { if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = "All anti-spam policies have internal sender notifications disabled.`n`n" - $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + $Result = [System.Text.StringBuilder]::new("All anti-spam policies have internal sender notifications disabled.`n`n") + $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)") } else { $Status = 'Failed' - $Result = "$($FailedPolicies.Count) anti-spam policies have internal sender notifications enabled.`n`n" - $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" - $Result += "| Policy Name | Inline Safety Tips Enabled |`n" - $Result += "|------------|---------------------------|`n" + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) anti-spam policies have internal sender notifications enabled.`n`n") + $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") + $null = $Result.Append("| Policy Name | Inline Safety Tips Enabled |`n") + $null = $Result.Append("|------------|---------------------------|`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.Identity) | $($Policy.InlineSafetyTipsEnabled) |`n" + $null = $Result.Append("| $($Policy.Identity) | $($Policy.InlineSafetyTipsEnabled) |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA111.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA111.ps1 index c1fa95853c93..5f19c83b3827 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA111.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA111.ps1 @@ -26,16 +26,16 @@ function Invoke-CippTestORCA111 { if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = "All anti-phishing policies have unauthenticated sender tagging enabled.`n`n" - $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + $Result = [System.Text.StringBuilder]::new("All anti-phishing policies have unauthenticated sender tagging enabled.`n`n") + $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)") } else { $Status = 'Failed' - $Result = "$($FailedPolicies.Count) anti-phishing policies do not have unauthenticated sender tagging enabled.`n`n" - $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" - $Result += "| Policy Name | Enable Unauthenticated Sender |`n" - $Result += "|------------|------------------------------|`n" + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) anti-phishing policies do not have unauthenticated sender tagging enabled.`n`n") + $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") + $null = $Result.Append("| Policy Name | Enable Unauthenticated Sender |`n") + $null = $Result.Append("|------------|------------------------------|`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.Identity) | $($Policy.EnableUnauthenticatedSender) |`n" + $null = $Result.Append("| $($Policy.Identity) | $($Policy.EnableUnauthenticatedSender) |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA112.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA112.ps1 index b1312a225698..98b53e4e193b 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA112.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA112.ps1 @@ -26,16 +26,16 @@ function Invoke-CippTestORCA112 { if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = "All anti-phishing policies have anti-spoofing action set to Move to Junk Email folder.`n`n" - $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + $Result = [System.Text.StringBuilder]::new("All anti-phishing policies have anti-spoofing action set to Move to Junk Email folder.`n`n") + $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)") } else { $Status = 'Failed' - $Result = "$($FailedPolicies.Count) anti-phishing policies do not have anti-spoofing action set to Move to Junk Email folder.`n`n" - $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" - $Result += "| Policy Name | Authentication Fail Action |`n" - $Result += "|------------|---------------------------|`n" + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) anti-phishing policies do not have anti-spoofing action set to Move to Junk Email folder.`n`n") + $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") + $null = $Result.Append("| Policy Name | Authentication Fail Action |`n") + $null = $Result.Append("|------------|---------------------------|`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.Identity) | $($Policy.AuthenticationFailAction) |`n" + $null = $Result.Append("| $($Policy.Identity) | $($Policy.AuthenticationFailAction) |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA113.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA113.ps1 index 1f19a2c52a49..a6f5d30b681b 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA113.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA113.ps1 @@ -27,26 +27,26 @@ function Invoke-CippTestORCA113 { if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = "All Safe Links policies have click-through disabled (DoNotAllowClickThrough = true).`n`n" - $Result += "**Compliant Policies:** $($PassedPolicies.Count)`n`n" + $Result = [System.Text.StringBuilder]::new("All Safe Links policies have click-through disabled (DoNotAllowClickThrough = true).`n`n") + $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)`n`n") if ($PassedPolicies.Count -gt 0) { - $Result += "| Policy Name | DoNotAllowClickThrough |`n" - $Result += "|------------|----------------------|`n" + $null = $Result.Append("| Policy Name | DoNotAllowClickThrough |`n") + $null = $Result.Append("|------------|----------------------|`n") foreach ($Policy in $PassedPolicies) { - $Result += "| $($Policy.Identity) | $($Policy.DoNotAllowClickThrough) |`n" + $null = $Result.Append("| $($Policy.Identity) | $($Policy.DoNotAllowClickThrough) |`n") } } } else { $Status = 'Failed' - $Result = "Some Safe Links policies allow click-through, which reduces protection.`n`n" - $Result += "**Failed Policies:** $($FailedPolicies.Count) | **Passed Policies:** $($PassedPolicies.Count)`n`n" - $Result += "### Non-Compliant Policies`n`n" - $Result += "| Policy Name | DoNotAllowClickThrough | Recommended |`n" - $Result += "|------------|----------------------|-------------|`n" + $Result = [System.Text.StringBuilder]::new("Some Safe Links policies allow click-through, which reduces protection.`n`n") + $null = $Result.Append("**Failed Policies:** $($FailedPolicies.Count) | **Passed Policies:** $($PassedPolicies.Count)`n`n") + $null = $Result.Append("### Non-Compliant Policies`n`n") + $null = $Result.Append("| Policy Name | DoNotAllowClickThrough | Recommended |`n") + $null = $Result.Append("|------------|----------------------|-------------|`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.Identity) | $($Policy.DoNotAllowClickThrough) | true |`n" + $null = $Result.Append("| $($Policy.Identity) | $($Policy.DoNotAllowClickThrough) | true |`n") } - $Result += "`n**Remediation:** Disable click-through (set DoNotAllowClickThrough to true) to prevent users from bypassing Safe Links protection." + $null = $Result.Append("`n**Remediation:** Disable click-through (set DoNotAllowClickThrough to true) to prevent users from bypassing Safe Links protection.") } Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA113' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'AllowClickThrough is disabled in Safe Links policies' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Safe Links' diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA114.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA114.ps1 index 9fd8629c74d0..921ba1dad27b 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA114.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA114.ps1 @@ -28,17 +28,17 @@ $FailedPolicies = [System.Collections.Generic.List[object]]::new() if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = "No anti-spam policies have IP allow lists configured.`n`n" - $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + $Result = [System.Text.StringBuilder]::new("No anti-spam policies have IP allow lists configured.`n`n") + $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)") } else { $Status = 'Failed' - $Result = "$($FailedPolicies.Count) anti-spam policies have IP allow lists configured.`n`n" - $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" - $Result += "| Policy Name | IP Allow List Count |`n" - $Result += "|------------|-------------------|`n" + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) anti-spam policies have IP allow lists configured.`n`n") + $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") + $null = $Result.Append("| Policy Name | IP Allow List Count |`n") + $null = $Result.Append("|------------|-------------------|`n") foreach ($Policy in $FailedPolicies) { $IPCount = if ($Policy.IPAllowList) { $Policy.IPAllowList.Count } else { 0 } - $Result += "| $($Policy.Identity) | $IPCount |`n" + $null = $Result.Append("| $($Policy.Identity) | $IPCount |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA115.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA115.ps1 index 589876fb598e..520459434633 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA115.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA115.ps1 @@ -26,16 +26,16 @@ function Invoke-CippTestORCA115 { if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = "All anti-phishing policies have mailbox intelligence based impersonation protection enabled.`n`n" - $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + $Result = [System.Text.StringBuilder]::new("All anti-phishing policies have mailbox intelligence based impersonation protection enabled.`n`n") + $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)") } else { $Status = 'Failed' - $Result = "$($FailedPolicies.Count) anti-phishing policies do not have mailbox intelligence based impersonation protection enabled.`n`n" - $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" - $Result += "| Policy Name | Enable Mailbox Intelligence Protection |`n" - $Result += "|------------|---------------------------------------|`n" + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) anti-phishing policies do not have mailbox intelligence based impersonation protection enabled.`n`n") + $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") + $null = $Result.Append("| Policy Name | Enable Mailbox Intelligence Protection |`n") + $null = $Result.Append("|------------|---------------------------------------|`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.Identity) | $($Policy.EnableMailboxIntelligenceProtection) |`n" + $null = $Result.Append("| $($Policy.Identity) | $($Policy.EnableMailboxIntelligenceProtection) |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA116.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA116.ps1 index acea46f26a58..a34da227c484 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA116.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA116.ps1 @@ -26,16 +26,16 @@ function Invoke-CippTestORCA116 { if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = "All anti-phishing policies have mailbox intelligence impersonation protection action set to Move to Junk Email folder.`n`n" - $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + $Result = [System.Text.StringBuilder]::new("All anti-phishing policies have mailbox intelligence impersonation protection action set to Move to Junk Email folder.`n`n") + $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)") } else { $Status = 'Failed' - $Result = "$($FailedPolicies.Count) anti-phishing policies do not have mailbox intelligence impersonation protection action set to Move to Junk Email folder.`n`n" - $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" - $Result += "| Policy Name | Mailbox Intelligence Protection Action |`n" - $Result += "|------------|---------------------------------------|`n" + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) anti-phishing policies do not have mailbox intelligence impersonation protection action set to Move to Junk Email folder.`n`n") + $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") + $null = $Result.Append("| Policy Name | Mailbox Intelligence Protection Action |`n") + $null = $Result.Append("|------------|---------------------------------------|`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.Identity) | $($Policy.MailboxIntelligenceProtectionAction) |`n" + $null = $Result.Append("| $($Policy.Identity) | $($Policy.MailboxIntelligenceProtectionAction) |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_1.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_1.ps1 index 3d83b616463b..f4fa2d75c382 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_1.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_1.ps1 @@ -28,17 +28,17 @@ function Invoke-CippTestORCA118_1 { if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = "No anti-spam policies have allowed sender domains configured.`n`n" - $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + $Result = [System.Text.StringBuilder]::new("No anti-spam policies have allowed sender domains configured.`n`n") + $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)") } else { $Status = 'Failed' - $Result = "$($FailedPolicies.Count) anti-spam policies have allowed sender domains configured.`n`n" - $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" - $Result += "| Policy Name | Allowed Sender Domains Count |`n" - $Result += "|------------|------------------------------|`n" + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) anti-spam policies have allowed sender domains configured.`n`n") + $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") + $null = $Result.Append("| Policy Name | Allowed Sender Domains Count |`n") + $null = $Result.Append("|------------|------------------------------|`n") foreach ($Policy in $FailedPolicies) { $Count = if ($Policy.AllowedSenderDomains) { $Policy.AllowedSenderDomains.Count } else { 0 } - $Result += "| $($Policy.Identity) | $Count |`n" + $null = $Result.Append("| $($Policy.Identity) | $Count |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_2.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_2.ps1 index 64c079f40f9a..76b8db06094c 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_2.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_2.ps1 @@ -24,17 +24,17 @@ function Invoke-CippTestORCA118_2 { if ($FailedRules.Count -eq 0) { $Status = 'Passed' - $Result = "No transport rules allow list domains by setting SCL to -1.`n`n" - $Result += "**Total Transport Rules Checked:** $($TransportRules.Count)" + $Result = [System.Text.StringBuilder]::new("No transport rules allow list domains by setting SCL to -1.`n`n") + $null = $Result.Append("**Total Transport Rules Checked:** $($TransportRules.Count)") } else { $Status = 'Failed' - $Result = "$($FailedRules.Count) transport rules allow list domains by setting SCL to -1.`n`n" - $Result += "**Non-Compliant Rules:** $($FailedRules.Count)`n`n" - $Result += "| Rule Name | Sender Domains |`n" - $Result += "|-----------|---------------|`n" + $Result = [System.Text.StringBuilder]::new("$($FailedRules.Count) transport rules allow list domains by setting SCL to -1.`n`n") + $null = $Result.Append("**Non-Compliant Rules:** $($FailedRules.Count)`n`n") + $null = $Result.Append("| Rule Name | Sender Domains |`n") + $null = $Result.Append("|-----------|---------------|`n") foreach ($Rule in $FailedRules) { $Domains = if ($Rule.SenderDomainIs) { ($Rule.SenderDomainIs -join ', ') } else { 'N/A' } - $Result += "| $($Rule.Name) | $Domains |`n" + $null = $Result.Append("| $($Rule.Name) | $Domains |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_3.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_3.ps1 index dba391a02923..ffdfdc713135 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_3.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_3.ps1 @@ -44,17 +44,17 @@ function Invoke-CippTestORCA118_3 { if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = "No anti-spam policies have own domains in the allow list.`n`n" - $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + $Result = [System.Text.StringBuilder]::new("No anti-spam policies have own domains in the allow list.`n`n") + $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)") } else { $Status = 'Failed' - $Result = "$($FailedPolicies.Count) anti-spam policies have own domains in the allow list.`n`n" - $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" - $Result += "| Policy Name | Own Domains in Allow List |`n" - $Result += "|------------|---------------------------|`n" + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) anti-spam policies have own domains in the allow list.`n`n") + $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") + $null = $Result.Append("| Policy Name | Own Domains in Allow List |`n") + $null = $Result.Append("|------------|---------------------------|`n") foreach ($Policy in $FailedPolicies) { $OwnDomainsInList = $Policy.AllowedSenderDomains | Where-Object { $OwnDomains -contains $_ } - $Result += "| $($Policy.Identity) | $($OwnDomainsInList -join ', ') |`n" + $null = $Result.Append("| $($Policy.Identity) | $($OwnDomainsInList -join ', ') |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_4.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_4.ps1 index 08a49d21713b..d953d21670e5 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_4.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_4.ps1 @@ -41,17 +41,17 @@ function Invoke-CippTestORCA118_4 { if ($FailedRules.Count -eq 0) { $Status = 'Passed' - $Result = "No transport rules allow list own domains by setting SCL to -1.`n`n" - $Result += "**Total Transport Rules Checked:** $($TransportRules.Count)" + $Result = [System.Text.StringBuilder]::new("No transport rules allow list own domains by setting SCL to -1.`n`n") + $null = $Result.Append("**Total Transport Rules Checked:** $($TransportRules.Count)") } else { $Status = 'Failed' - $Result = "$($FailedRules.Count) transport rules allow list own domains by setting SCL to -1.`n`n" - $Result += "**Non-Compliant Rules:** $($FailedRules.Count)`n`n" - $Result += "| Rule Name | Own Domains in Rule |`n" - $Result += "|-----------|-------------------|`n" + $Result = [System.Text.StringBuilder]::new("$($FailedRules.Count) transport rules allow list own domains by setting SCL to -1.`n`n") + $null = $Result.Append("**Non-Compliant Rules:** $($FailedRules.Count)`n`n") + $null = $Result.Append("| Rule Name | Own Domains in Rule |`n") + $null = $Result.Append("|-----------|-------------------|`n") foreach ($Rule in $FailedRules) { $OwnDomainsInRule = $Rule.SenderDomainIs | Where-Object { $OwnDomains -contains $_ } - $Result += "| $($Rule.Name) | $($OwnDomainsInRule -join ', ') |`n" + $null = $Result.Append("| $($Rule.Name) | $($OwnDomainsInRule -join ', ') |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA119.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA119.ps1 index 15b1ce1804cf..872bc1744665 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA119.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA119.ps1 @@ -26,16 +26,16 @@ function Invoke-CippTestORCA119 { if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = "All anti-phishing policies have Similar Domains Safety Tips enabled.`n`n" - $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + $Result = [System.Text.StringBuilder]::new("All anti-phishing policies have Similar Domains Safety Tips enabled.`n`n") + $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)") } else { $Status = 'Failed' - $Result = "$($FailedPolicies.Count) anti-phishing policies do not have Similar Domains Safety Tips enabled.`n`n" - $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" - $Result += "| Policy Name | Enable Similar Domains Safety Tips |`n" - $Result += "|------------|-----------------------------------|`n" + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) anti-phishing policies do not have Similar Domains Safety Tips enabled.`n`n") + $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") + $null = $Result.Append("| Policy Name | Enable Similar Domains Safety Tips |`n") + $null = $Result.Append("|------------|-----------------------------------|`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.Identity) | $($Policy.EnableSimilarDomainsSafetyTips) |`n" + $null = $Result.Append("| $($Policy.Identity) | $($Policy.EnableSimilarDomainsSafetyTips) |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA120_malware.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA120_malware.ps1 index 2fabda7833fb..5c40f7398aa6 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA120_malware.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA120_malware.ps1 @@ -26,16 +26,16 @@ function Invoke-CippTestORCA120_malware { if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = "All malware filter policies have Zero Hour Autopurge enabled.`n`n" - $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + $Result = [System.Text.StringBuilder]::new("All malware filter policies have Zero Hour Autopurge enabled.`n`n") + $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)") } else { $Status = 'Failed' - $Result = "$($FailedPolicies.Count) malware filter policies do not have Zero Hour Autopurge enabled.`n`n" - $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" - $Result += "| Policy Name | ZAP Enabled |`n" - $Result += "|------------|------------|`n" + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) malware filter policies do not have Zero Hour Autopurge enabled.`n`n") + $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") + $null = $Result.Append("| Policy Name | ZAP Enabled |`n") + $null = $Result.Append("|------------|------------|`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.Identity) | $($Policy.ZapEnabled) |`n" + $null = $Result.Append("| $($Policy.Identity) | $($Policy.ZapEnabled) |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA120_phish.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA120_phish.ps1 index 8e7a42135d36..82c1244f8ca4 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA120_phish.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA120_phish.ps1 @@ -26,16 +26,16 @@ function Invoke-CippTestORCA120_phish { if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = "All anti-spam policies have Zero Hour Autopurge for Phish enabled.`n`n" - $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + $Result = [System.Text.StringBuilder]::new("All anti-spam policies have Zero Hour Autopurge for Phish enabled.`n`n") + $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)") } else { $Status = 'Failed' - $Result = "$($FailedPolicies.Count) anti-spam policies do not have Zero Hour Autopurge for Phish enabled.`n`n" - $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" - $Result += "| Policy Name | Phish ZAP Enabled |`n" - $Result += "|------------|------------------|`n" + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) anti-spam policies do not have Zero Hour Autopurge for Phish enabled.`n`n") + $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") + $null = $Result.Append("| Policy Name | Phish ZAP Enabled |`n") + $null = $Result.Append("|------------|------------------|`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.Identity) | $($Policy.PhishZapEnabled) |`n" + $null = $Result.Append("| $($Policy.Identity) | $($Policy.PhishZapEnabled) |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA120_spam.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA120_spam.ps1 index b09403c2fed7..3f19a6ddb540 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA120_spam.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA120_spam.ps1 @@ -26,16 +26,16 @@ function Invoke-CippTestORCA120_spam { if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = "All anti-spam policies have Zero Hour Autopurge for Spam enabled.`n`n" - $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + $Result = [System.Text.StringBuilder]::new("All anti-spam policies have Zero Hour Autopurge for Spam enabled.`n`n") + $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)") } else { $Status = 'Failed' - $Result = "$($FailedPolicies.Count) anti-spam policies do not have Zero Hour Autopurge for Spam enabled.`n`n" - $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" - $Result += "| Policy Name | Spam ZAP Enabled |`n" - $Result += "|------------|-----------------|`n" + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) anti-spam policies do not have Zero Hour Autopurge for Spam enabled.`n`n") + $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") + $null = $Result.Append("| Policy Name | Spam ZAP Enabled |`n") + $null = $Result.Append("|------------|-----------------|`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.Identity) | $($Policy.SpamZapEnabled) |`n" + $null = $Result.Append("| $($Policy.Identity) | $($Policy.SpamZapEnabled) |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA121.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA121.ps1 index 1a0aa4ec7523..d18f29b16a7c 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA121.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA121.ps1 @@ -14,8 +14,8 @@ function Invoke-CippTestORCA121 { } $Status = 'Passed' - $Result = "Quarantine policies are configured to support Zero Hour Auto Purge.`n`n" - $Result += "**Total Policies:** $($Policies.Count)" + $Result = [System.Text.StringBuilder]::new("Quarantine policies are configured to support Zero Hour Auto Purge.`n`n") + $null = $Result.Append("**Total Policies:** $($Policies.Count)") Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA121' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Low' -Name 'Supported filter policy action used' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Quarantine' diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA123.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA123.ps1 index 274e8c4cd810..f1690920bf62 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA123.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA123.ps1 @@ -26,16 +26,16 @@ function Invoke-CippTestORCA123 { if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = "All anti-phishing policies have Unusual Characters Safety Tips enabled.`n`n" - $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + $Result = [System.Text.StringBuilder]::new("All anti-phishing policies have Unusual Characters Safety Tips enabled.`n`n") + $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)") } else { $Status = 'Failed' - $Result = "$($FailedPolicies.Count) anti-phishing policies do not have Unusual Characters Safety Tips enabled.`n`n" - $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" - $Result += "| Policy Name | Enable Unusual Characters Safety Tips |`n" - $Result += "|------------|---------------------------------------|`n" + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) anti-phishing policies do not have Unusual Characters Safety Tips enabled.`n`n") + $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") + $null = $Result.Append("| Policy Name | Enable Unusual Characters Safety Tips |`n") + $null = $Result.Append("|------------|---------------------------------------|`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.Identity) | $($Policy.EnableUnusualCharactersSafetyTips) |`n" + $null = $Result.Append("| $($Policy.Identity) | $($Policy.EnableUnusualCharactersSafetyTips) |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA124.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA124.ps1 index 041ed8304074..a09a297734ba 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA124.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA124.ps1 @@ -26,16 +26,16 @@ function Invoke-CippTestORCA124 { if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = "All Safe Attachments policies have unknown malware response set to Block.`n`n" - $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + $Result = [System.Text.StringBuilder]::new("All Safe Attachments policies have unknown malware response set to Block.`n`n") + $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)") } else { $Status = 'Failed' - $Result = "$($FailedPolicies.Count) Safe Attachments policies do not have unknown malware response set to Block.`n`n" - $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" - $Result += "| Policy Name | Action |`n" - $Result += "|------------|--------|`n" + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) Safe Attachments policies do not have unknown malware response set to Block.`n`n") + $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") + $null = $Result.Append("| Policy Name | Action |`n") + $null = $Result.Append("|------------|--------|`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.Identity) | $($Policy.Action) |`n" + $null = $Result.Append("| $($Policy.Identity) | $($Policy.Action) |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA139.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA139.ps1 index ea31ca0bfc90..a2c7d81be2c5 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA139.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA139.ps1 @@ -26,16 +26,16 @@ function Invoke-CippTestORCA139 { if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = "All anti-spam policies have Spam action set to move to Junk Email folder or Quarantine.`n`n" - $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + $Result = [System.Text.StringBuilder]::new("All anti-spam policies have Spam action set to move to Junk Email folder or Quarantine.`n`n") + $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)") } else { $Status = 'Failed' - $Result = "$($FailedPolicies.Count) anti-spam policies do not have Spam action set appropriately.`n`n" - $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" - $Result += "| Policy Name | Spam Action |`n" - $Result += "|------------|------------|`n" + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) anti-spam policies do not have Spam action set appropriately.`n`n") + $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") + $null = $Result.Append("| Policy Name | Spam Action |`n") + $null = $Result.Append("|------------|------------|`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.Identity) | $($Policy.SpamAction) |`n" + $null = $Result.Append("| $($Policy.Identity) | $($Policy.SpamAction) |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA140.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA140.ps1 index af9d6bcfd607..9041c6fabbf8 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA140.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA140.ps1 @@ -26,16 +26,16 @@ function Invoke-CippTestORCA140 { if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = "All anti-spam policies have High Confidence Spam action set to Quarantine.`n`n" - $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + $Result = [System.Text.StringBuilder]::new("All anti-spam policies have High Confidence Spam action set to Quarantine.`n`n") + $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)") } else { $Status = 'Failed' - $Result = "$($FailedPolicies.Count) anti-spam policies do not have High Confidence Spam action set to Quarantine.`n`n" - $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" - $Result += "| Policy Name | High Confidence Spam Action |`n" - $Result += "|------------|---------------------------|`n" + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) anti-spam policies do not have High Confidence Spam action set to Quarantine.`n`n") + $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") + $null = $Result.Append("| Policy Name | High Confidence Spam Action |`n") + $null = $Result.Append("|------------|---------------------------|`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.Identity) | $($Policy.HighConfidenceSpamAction) |`n" + $null = $Result.Append("| $($Policy.Identity) | $($Policy.HighConfidenceSpamAction) |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA141.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA141.ps1 index d9698edb6515..f19ce8ece8d0 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA141.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA141.ps1 @@ -26,16 +26,16 @@ function Invoke-CippTestORCA141 { if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = "All anti-spam policies have Bulk action set to Move to Junk Email folder.`n`n" - $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + $Result = [System.Text.StringBuilder]::new("All anti-spam policies have Bulk action set to Move to Junk Email folder.`n`n") + $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)") } else { $Status = 'Failed' - $Result = "$($FailedPolicies.Count) anti-spam policies do not have Bulk action set to Move to Junk Email folder.`n`n" - $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" - $Result += "| Policy Name | Bulk Spam Action |`n" - $Result += "|------------|-----------------|`n" + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) anti-spam policies do not have Bulk action set to Move to Junk Email folder.`n`n") + $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") + $null = $Result.Append("| Policy Name | Bulk Spam Action |`n") + $null = $Result.Append("|------------|-----------------|`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.Identity) | $($Policy.BulkSpamAction) |`n" + $null = $Result.Append("| $($Policy.Identity) | $($Policy.BulkSpamAction) |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA142.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA142.ps1 index 2041970791e8..a85111e1578a 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA142.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA142.ps1 @@ -26,16 +26,16 @@ function Invoke-CippTestORCA142 { if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = "All anti-spam policies have Phish action set to Quarantine.`n`n" - $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + $Result = [System.Text.StringBuilder]::new("All anti-spam policies have Phish action set to Quarantine.`n`n") + $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)") } else { $Status = 'Failed' - $Result = "$($FailedPolicies.Count) anti-spam policies do not have Phish action set to Quarantine.`n`n" - $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" - $Result += "| Policy Name | Phish Spam Action |`n" - $Result += "|------------|------------------|`n" + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) anti-spam policies do not have Phish action set to Quarantine.`n`n") + $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") + $null = $Result.Append("| Policy Name | Phish Spam Action |`n") + $null = $Result.Append("|------------|------------------|`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.Identity) | $($Policy.PhishSpamAction) |`n" + $null = $Result.Append("| $($Policy.Identity) | $($Policy.PhishSpamAction) |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA143.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA143.ps1 index fc1ea3a3acbd..c19e61bc153b 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA143.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA143.ps1 @@ -26,16 +26,16 @@ function Invoke-CippTestORCA143 { if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = "All anti-spam policies have Safety Tips enabled.`n`n" - $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + $Result = [System.Text.StringBuilder]::new("All anti-spam policies have Safety Tips enabled.`n`n") + $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)") } else { $Status = 'Failed' - $Result = "$($FailedPolicies.Count) anti-spam policies do not have Safety Tips enabled.`n`n" - $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" - $Result += "| Policy Name | Inline Safety Tips Enabled |`n" - $Result += "|------------|---------------------------|`n" + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) anti-spam policies do not have Safety Tips enabled.`n`n") + $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") + $null = $Result.Append("| Policy Name | Inline Safety Tips Enabled |`n") + $null = $Result.Append("|------------|---------------------------|`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.Identity) | $($Policy.InlineSafetyTipsEnabled) |`n" + $null = $Result.Append("| $($Policy.Identity) | $($Policy.InlineSafetyTipsEnabled) |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA156.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA156.ps1 index 7cb949d8800e..54e346fb3116 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA156.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA156.ps1 @@ -26,16 +26,16 @@ function Invoke-CippTestORCA156 { if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = "All Safe Links policies are tracking user clicks.`n`n" - $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + $Result = [System.Text.StringBuilder]::new("All Safe Links policies are tracking user clicks.`n`n") + $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)") } else { $Status = 'Failed' - $Result = "$($FailedPolicies.Count) Safe Links policies are not tracking user clicks.`n`n" - $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" - $Result += "| Policy Name | Track Clicks |`n" - $Result += "|------------|-------------|`n" + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) Safe Links policies are not tracking user clicks.`n`n") + $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") + $null = $Result.Append("| Policy Name | Track Clicks |`n") + $null = $Result.Append("|------------|-------------|`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.Identity) | $($Policy.TrackClicks) |`n" + $null = $Result.Append("| $($Policy.Identity) | $($Policy.TrackClicks) |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA158.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA158.ps1 index 60765c5d2ea5..0bf527155b7a 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA158.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA158.ps1 @@ -17,12 +17,12 @@ function Invoke-CippTestORCA158 { if ($Policy.EnableATPForSPOTeamsODB -eq $true) { $Status = 'Passed' - $Result = "Safe Attachments is enabled for SharePoint, OneDrive, and Teams.`n`n" - $Result += "**EnableATPForSPOTeamsODB:** $($Policy.EnableATPForSPOTeamsODB)" + $Result = [System.Text.StringBuilder]::new("Safe Attachments is enabled for SharePoint, OneDrive, and Teams.`n`n") + $null = $Result.Append("**EnableATPForSPOTeamsODB:** $($Policy.EnableATPForSPOTeamsODB)") } else { $Status = 'Failed' - $Result = "Safe Attachments is NOT enabled for SharePoint, OneDrive, and Teams.`n`n" - $Result += "**EnableATPForSPOTeamsODB:** $($Policy.EnableATPForSPOTeamsODB)" + $Result = [System.Text.StringBuilder]::new("Safe Attachments is NOT enabled for SharePoint, OneDrive, and Teams.`n`n") + $null = $Result.Append("**EnableATPForSPOTeamsODB:** $($Policy.EnableATPForSPOTeamsODB)") } Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA158' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Safe Attachments enabled for SharePoint and Teams' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Safe Attachments' diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA179.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA179.ps1 index faf0fab99079..c2ea2cf19ed6 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA179.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA179.ps1 @@ -26,16 +26,16 @@ function Invoke-CippTestORCA179 { if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = "All Safe Links policies are enabled for internal senders.`n`n" - $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + $Result = [System.Text.StringBuilder]::new("All Safe Links policies are enabled for internal senders.`n`n") + $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)") } else { $Status = 'Failed' - $Result = "$($FailedPolicies.Count) Safe Links policies are not enabled for internal senders.`n`n" - $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" - $Result += "| Policy Name | Enable For Internal Senders |`n" - $Result += "|------------|----------------------------|`n" + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) Safe Links policies are not enabled for internal senders.`n`n") + $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") + $null = $Result.Append("| Policy Name | Enable For Internal Senders |`n") + $null = $Result.Append("|------------|----------------------------|`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.Identity) | $($Policy.EnableForInternalSenders) |`n" + $null = $Result.Append("| $($Policy.Identity) | $($Policy.EnableForInternalSenders) |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA180.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA180.ps1 index 7bd541c3be64..25bead866513 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA180.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA180.ps1 @@ -26,16 +26,16 @@ function Invoke-CippTestORCA180 { if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = "All anti-phishing policies have Spoof Intelligence enabled.`n`n" - $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + $Result = [System.Text.StringBuilder]::new("All anti-phishing policies have Spoof Intelligence enabled.`n`n") + $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)") } else { $Status = 'Failed' - $Result = "$($FailedPolicies.Count) anti-phishing policies do not have Spoof Intelligence enabled.`n`n" - $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" - $Result += "| Policy Name | Enable Spoof Intelligence |`n" - $Result += "|------------|--------------------------|`n" + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) anti-phishing policies do not have Spoof Intelligence enabled.`n`n") + $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") + $null = $Result.Append("| Policy Name | Enable Spoof Intelligence |`n") + $null = $Result.Append("|------------|--------------------------|`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.Identity) | $($Policy.EnableSpoofIntelligence) |`n" + $null = $Result.Append("| $($Policy.Identity) | $($Policy.EnableSpoofIntelligence) |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA189.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA189.ps1 index c2d4bb76cda1..d5618addeeac 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA189.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA189.ps1 @@ -22,14 +22,14 @@ function Invoke-CippTestORCA189 { if ($BypassRules.Count -eq 0) { $Status = 'Passed' - $Result = "No transport rules are bypassing Safe Attachments processing." + $Result = [System.Text.StringBuilder]::new("No transport rules are bypassing Safe Attachments processing.") } else { $Status = 'Failed' - $Result = "$($BypassRules.Count) transport rules are bypassing Safe Attachments processing.`n`n" - $Result += "| Rule Name | Priority |`n" - $Result += "|-----------|----------|`n" + $Result = [System.Text.StringBuilder]::new("$($BypassRules.Count) transport rules are bypassing Safe Attachments processing.`n`n") + $null = $Result.Append("| Rule Name | Priority |`n") + $null = $Result.Append("|-----------|----------|`n") foreach ($Rule in $BypassRules) { - $Result += "| $($Rule.Name) | $($Rule.Priority) |`n" + $null = $Result.Append("| $($Rule.Name) | $($Rule.Priority) |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA189_2.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA189_2.ps1 index f5eeb2ed4d47..ae5adf2160dd 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA189_2.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA189_2.ps1 @@ -22,14 +22,14 @@ function Invoke-CippTestORCA189_2 { if ($BypassRules.Count -eq 0) { $Status = 'Passed' - $Result = "No transport rules are bypassing Safe Links processing." + $Result = [System.Text.StringBuilder]::new("No transport rules are bypassing Safe Links processing.") } else { $Status = 'Failed' - $Result = "$($BypassRules.Count) transport rules are bypassing Safe Links processing.`n`n" - $Result += "| Rule Name | Priority |`n" - $Result += "|-----------|----------|`n" + $Result = [System.Text.StringBuilder]::new("$($BypassRules.Count) transport rules are bypassing Safe Links processing.`n`n") + $null = $Result.Append("| Rule Name | Priority |`n") + $null = $Result.Append("|-----------|----------|`n") foreach ($Rule in $BypassRules) { - $Result += "| $($Rule.Name) | $($Rule.Priority) |`n" + $null = $Result.Append("| $($Rule.Name) | $($Rule.Priority) |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA205.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA205.ps1 index bf6b8e732fa8..cfe5ce687551 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA205.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA205.ps1 @@ -26,16 +26,16 @@ function Invoke-CippTestORCA205 { if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = "All malware filter policies have common attachment type filter enabled.`n`n" - $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + $Result = [System.Text.StringBuilder]::new("All malware filter policies have common attachment type filter enabled.`n`n") + $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)") } else { $Status = 'Failed' - $Result = "$($FailedPolicies.Count) malware filter policies do not have common attachment type filter enabled.`n`n" - $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" - $Result += "| Policy Name | Enable File Filter |`n" - $Result += "|------------|-------------------|`n" + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) malware filter policies do not have common attachment type filter enabled.`n`n") + $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") + $null = $Result.Append("| Policy Name | Enable File Filter |`n") + $null = $Result.Append("|------------|-------------------|`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.Identity) | $($Policy.EnableFileFilter) |`n" + $null = $Result.Append("| $($Policy.Identity) | $($Policy.EnableFileFilter) |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA220.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA220.ps1 index e805a5afad39..2fe281429948 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA220.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA220.ps1 @@ -27,16 +27,16 @@ function Invoke-CippTestORCA220 { if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = "All anti-phishing policies have adequate phishing threshold levels (2 or higher).`n`n" - $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + $Result = [System.Text.StringBuilder]::new("All anti-phishing policies have adequate phishing threshold levels (2 or higher).`n`n") + $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)") } else { $Status = 'Failed' - $Result = "$($FailedPolicies.Count) anti-phishing policies have inadequate phishing threshold levels.`n`n" - $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" - $Result += "| Policy Name | Phish Threshold Level |`n" - $Result += "|------------|----------------------|`n" + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) anti-phishing policies have inadequate phishing threshold levels.`n`n") + $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") + $null = $Result.Append("| Policy Name | Phish Threshold Level |`n") + $null = $Result.Append("|------------|----------------------|`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.Identity) | $($Policy.PhishThresholdLevel) |`n" + $null = $Result.Append("| $($Policy.Identity) | $($Policy.PhishThresholdLevel) |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA221.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA221.ps1 index ec51d22b4575..3a4e73ffcf9b 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA221.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA221.ps1 @@ -26,16 +26,16 @@ function Invoke-CippTestORCA221 { if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = "All anti-phishing policies have mailbox intelligence enabled.`n`n" - $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + $Result = [System.Text.StringBuilder]::new("All anti-phishing policies have mailbox intelligence enabled.`n`n") + $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)") } else { $Status = 'Failed' - $Result = "$($FailedPolicies.Count) anti-phishing policies do not have mailbox intelligence enabled.`n`n" - $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" - $Result += "| Policy Name | Enable Mailbox Intelligence |`n" - $Result += "|------------|----------------------------|`n" + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) anti-phishing policies do not have mailbox intelligence enabled.`n`n") + $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") + $null = $Result.Append("| Policy Name | Enable Mailbox Intelligence |`n") + $null = $Result.Append("|------------|----------------------------|`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.Identity) | $($Policy.EnableMailboxIntelligence) |`n" + $null = $Result.Append("| $($Policy.Identity) | $($Policy.EnableMailboxIntelligence) |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA222.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA222.ps1 index 184f047d72b8..b28d794331d0 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA222.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA222.ps1 @@ -26,16 +26,16 @@ function Invoke-CippTestORCA222 { if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = "All anti-phishing policies have Domain Impersonation action set to Quarantine.`n`n" - $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + $Result = [System.Text.StringBuilder]::new("All anti-phishing policies have Domain Impersonation action set to Quarantine.`n`n") + $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)") } else { $Status = 'Failed' - $Result = "$($FailedPolicies.Count) anti-phishing policies do not have Domain Impersonation action set to Quarantine.`n`n" - $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" - $Result += "| Policy Name | Targeted Domain Protection Action |`n" - $Result += "|------------|----------------------------------|`n" + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) anti-phishing policies do not have Domain Impersonation action set to Quarantine.`n`n") + $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") + $null = $Result.Append("| Policy Name | Targeted Domain Protection Action |`n") + $null = $Result.Append("|------------|----------------------------------|`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.Identity) | $($Policy.TargetedDomainProtectionAction) |`n" + $null = $Result.Append("| $($Policy.Identity) | $($Policy.TargetedDomainProtectionAction) |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA223.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA223.ps1 index 034a0af3980d..861687b05c67 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA223.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA223.ps1 @@ -26,16 +26,16 @@ function Invoke-CippTestORCA223 { if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = "All anti-phishing policies have User Impersonation action set to Quarantine.`n`n" - $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + $Result = [System.Text.StringBuilder]::new("All anti-phishing policies have User Impersonation action set to Quarantine.`n`n") + $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)") } else { $Status = 'Failed' - $Result = "$($FailedPolicies.Count) anti-phishing policies do not have User Impersonation action set to Quarantine.`n`n" - $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" - $Result += "| Policy Name | Targeted User Protection Action |`n" - $Result += "|------------|--------------------------------|`n" + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) anti-phishing policies do not have User Impersonation action set to Quarantine.`n`n") + $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") + $null = $Result.Append("| Policy Name | Targeted User Protection Action |`n") + $null = $Result.Append("|------------|--------------------------------|`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.Identity) | $($Policy.TargetedUserProtectionAction) |`n" + $null = $Result.Append("| $($Policy.Identity) | $($Policy.TargetedUserProtectionAction) |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA224.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA224.ps1 index 6ab7c9533dde..e50ca47e8309 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA224.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA224.ps1 @@ -26,16 +26,16 @@ function Invoke-CippTestORCA224 { if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = "All anti-phishing policies have Similar Users Safety Tips enabled.`n`n" - $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + $Result = [System.Text.StringBuilder]::new("All anti-phishing policies have Similar Users Safety Tips enabled.`n`n") + $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)") } else { $Status = 'Failed' - $Result = "$($FailedPolicies.Count) anti-phishing policies do not have Similar Users Safety Tips enabled.`n`n" - $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" - $Result += "| Policy Name | Enable Similar Users Safety Tips |`n" - $Result += "|------------|----------------------------------|`n" + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) anti-phishing policies do not have Similar Users Safety Tips enabled.`n`n") + $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") + $null = $Result.Append("| Policy Name | Enable Similar Users Safety Tips |`n") + $null = $Result.Append("|------------|----------------------------------|`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.Identity) | $($Policy.EnableSimilarUsersSafetyTips) |`n" + $null = $Result.Append("| $($Policy.Identity) | $($Policy.EnableSimilarUsersSafetyTips) |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA225.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA225.ps1 index 25a1bd09b827..487b3af7820e 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA225.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA225.ps1 @@ -17,12 +17,12 @@ function Invoke-CippTestORCA225 { if ($Policy.EnableSafeDocs -eq $true) { $Status = 'Passed' - $Result = "Safe Documents is enabled for Office clients.`n`n" - $Result += "**EnableSafeDocs:** $($Policy.EnableSafeDocs)" + $Result = [System.Text.StringBuilder]::new("Safe Documents is enabled for Office clients.`n`n") + $null = $Result.Append("**EnableSafeDocs:** $($Policy.EnableSafeDocs)") } else { $Status = 'Failed' - $Result = "Safe Documents is NOT enabled for Office clients.`n`n" - $Result += "**EnableSafeDocs:** $($Policy.EnableSafeDocs)" + $Result = [System.Text.StringBuilder]::new("Safe Documents is NOT enabled for Office clients.`n`n") + $null = $Result.Append("**EnableSafeDocs:** $($Policy.EnableSafeDocs)") } Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA225' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Safe Documents is enabled for Office clients' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Safe Attachments' diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA226.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA226.ps1 index 3293fc49bb0f..a25c716e83bf 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA226.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA226.ps1 @@ -38,15 +38,15 @@ function Invoke-CippTestORCA226 { if ($DomainsWithoutPolicy.Count -eq 0) { $Status = 'Passed' - $Result = "All accepted domains are covered by Safe Links policies.`n`n" - $Result += "**Total Accepted Domains:** $($AcceptedDomains.Count)`n" - $Result += "**Total Safe Links Policies:** $($SafeLinksPolicies.Count)" + $Result = [System.Text.StringBuilder]::new("All accepted domains are covered by Safe Links policies.`n`n") + $null = $Result.Append("**Total Accepted Domains:** $($AcceptedDomains.Count)`n") + $null = $Result.Append("**Total Safe Links Policies:** $($SafeLinksPolicies.Count)") } else { $Status = 'Failed' - $Result = "$($DomainsWithoutPolicy.Count) domains do not have a Safe Links policy.`n`n" - $Result += "**Domains Without Policy:**`n`n" + $Result = [System.Text.StringBuilder]::new("$($DomainsWithoutPolicy.Count) domains do not have a Safe Links policy.`n`n") + $null = $Result.Append("**Domains Without Policy:**`n`n") foreach ($Domain in $DomainsWithoutPolicy) { - $Result += "- $Domain`n" + $null = $Result.Append("- $Domain`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA227.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA227.ps1 index 8ec0b4a589a9..519d060dbf1c 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA227.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA227.ps1 @@ -38,15 +38,15 @@ function Invoke-CippTestORCA227 { if ($DomainsWithoutPolicy.Count -eq 0) { $Status = 'Passed' - $Result = "All accepted domains are covered by Safe Attachments policies.`n`n" - $Result += "**Total Accepted Domains:** $($AcceptedDomains.Count)`n" - $Result += "**Total Safe Attachments Policies:** $($SafeAttachmentPolicies.Count)" + $Result = [System.Text.StringBuilder]::new("All accepted domains are covered by Safe Attachments policies.`n`n") + $null = $Result.Append("**Total Accepted Domains:** $($AcceptedDomains.Count)`n") + $null = $Result.Append("**Total Safe Attachments Policies:** $($SafeAttachmentPolicies.Count)") } else { $Status = 'Failed' - $Result = "$($DomainsWithoutPolicy.Count) domains do not have a Safe Attachments policy.`n`n" - $Result += "**Domains Without Policy:**`n`n" + $Result = [System.Text.StringBuilder]::new("$($DomainsWithoutPolicy.Count) domains do not have a Safe Attachments policy.`n`n") + $null = $Result.Append("**Domains Without Policy:**`n`n") foreach ($Domain in $DomainsWithoutPolicy) { - $Result += "- $Domain`n" + $null = $Result.Append("- $Domain`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA228.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA228.ps1 index 9cda62f6721f..932a544e49ed 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA228.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA228.ps1 @@ -28,17 +28,17 @@ function Invoke-CippTestORCA228 { if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = "No anti-phishing policies have trusted senders configured.`n`n" - $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + $Result = [System.Text.StringBuilder]::new("No anti-phishing policies have trusted senders configured.`n`n") + $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)") } else { $Status = 'Failed' - $Result = "$($FailedPolicies.Count) anti-phishing policies have trusted senders configured.`n`n" - $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" - $Result += "| Policy Name | Excluded Senders Count |`n" - $Result += "|------------|----------------------|`n" + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) anti-phishing policies have trusted senders configured.`n`n") + $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") + $null = $Result.Append("| Policy Name | Excluded Senders Count |`n") + $null = $Result.Append("|------------|----------------------|`n") foreach ($Policy in $FailedPolicies) { $Count = if ($Policy.ExcludedSenders) { $Policy.ExcludedSenders.Count } else { 0 } - $Result += "| $($Policy.Identity) | $Count |`n" + $null = $Result.Append("| $($Policy.Identity) | $Count |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA229.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA229.ps1 index 8ec32aeae7ba..94ade900dfca 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA229.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA229.ps1 @@ -28,17 +28,17 @@ function Invoke-CippTestORCA229 { if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = "No anti-phishing policies have trusted domains configured.`n`n" - $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + $Result = [System.Text.StringBuilder]::new("No anti-phishing policies have trusted domains configured.`n`n") + $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)") } else { $Status = 'Failed' - $Result = "$($FailedPolicies.Count) anti-phishing policies have trusted domains configured.`n`n" - $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" - $Result += "| Policy Name | Excluded Domains Count |`n" - $Result += "|------------|----------------------|`n" + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) anti-phishing policies have trusted domains configured.`n`n") + $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") + $null = $Result.Append("| Policy Name | Excluded Domains Count |`n") + $null = $Result.Append("|------------|----------------------|`n") foreach ($Policy in $FailedPolicies) { $Count = if ($Policy.ExcludedDomains) { $Policy.ExcludedDomains.Count } else { 0 } - $Result += "| $($Policy.Identity) | $Count |`n" + $null = $Result.Append("| $($Policy.Identity) | $Count |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA230.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA230.ps1 index 434dbd2a1376..37b89f62c44f 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA230.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA230.ps1 @@ -38,15 +38,15 @@ function Invoke-CippTestORCA230 { if ($DomainsWithoutPolicy.Count -eq 0) { $Status = 'Passed' - $Result = "All accepted domains are covered by Anti-phishing policies.`n`n" - $Result += "**Total Accepted Domains:** $($AcceptedDomains.Count)`n" - $Result += "**Total Anti-phishing Policies:** $($AntiPhishPolicies.Count)" + $Result = [System.Text.StringBuilder]::new("All accepted domains are covered by Anti-phishing policies.`n`n") + $null = $Result.Append("**Total Accepted Domains:** $($AcceptedDomains.Count)`n") + $null = $Result.Append("**Total Anti-phishing Policies:** $($AntiPhishPolicies.Count)") } else { $Status = 'Failed' - $Result = "$($DomainsWithoutPolicy.Count) domains do not have an Anti-phishing policy.`n`n" - $Result += "**Domains Without Policy:**`n`n" + $Result = [System.Text.StringBuilder]::new("$($DomainsWithoutPolicy.Count) domains do not have an Anti-phishing policy.`n`n") + $null = $Result.Append("**Domains Without Policy:**`n`n") foreach ($Domain in $DomainsWithoutPolicy) { - $Result += "- $Domain`n" + $null = $Result.Append("- $Domain`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA231.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA231.ps1 index 10803fd0a5d4..b9a01cd1529a 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA231.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA231.ps1 @@ -38,15 +38,15 @@ function Invoke-CippTestORCA231 { if ($DomainsWithoutPolicy.Count -eq 0) { $Status = 'Passed' - $Result = "All accepted domains are covered by anti-spam policies.`n`n" - $Result += "**Total Accepted Domains:** $($AcceptedDomains.Count)`n" - $Result += "**Total Anti-spam Policies:** $($ContentFilterPolicies.Count)" + $Result = [System.Text.StringBuilder]::new("All accepted domains are covered by anti-spam policies.`n`n") + $null = $Result.Append("**Total Accepted Domains:** $($AcceptedDomains.Count)`n") + $null = $Result.Append("**Total Anti-spam Policies:** $($ContentFilterPolicies.Count)") } else { $Status = 'Failed' - $Result = "$($DomainsWithoutPolicy.Count) domains do not have an anti-spam policy.`n`n" - $Result += "**Domains Without Policy:**`n`n" + $Result = [System.Text.StringBuilder]::new("$($DomainsWithoutPolicy.Count) domains do not have an anti-spam policy.`n`n") + $null = $Result.Append("**Domains Without Policy:**`n`n") foreach ($Domain in $DomainsWithoutPolicy) { - $Result += "- $Domain`n" + $null = $Result.Append("- $Domain`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA232.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA232.ps1 index f65ae48d5ec3..280d54cc0877 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA232.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA232.ps1 @@ -38,15 +38,15 @@ function Invoke-CippTestORCA232 { if ($DomainsWithoutPolicy.Count -eq 0) { $Status = 'Passed' - $Result = "All accepted domains are covered by malware filter policies.`n`n" - $Result += "**Total Accepted Domains:** $($AcceptedDomains.Count)`n" - $Result += "**Total Malware Filter Policies:** $($MalwarePolicies.Count)" + $Result = [System.Text.StringBuilder]::new("All accepted domains are covered by malware filter policies.`n`n") + $null = $Result.Append("**Total Accepted Domains:** $($AcceptedDomains.Count)`n") + $null = $Result.Append("**Total Malware Filter Policies:** $($MalwarePolicies.Count)") } else { $Status = 'Failed' - $Result = "$($DomainsWithoutPolicy.Count) domains do not have a malware filter policy.`n`n" - $Result += "**Domains Without Policy:**`n`n" + $Result = [System.Text.StringBuilder]::new("$($DomainsWithoutPolicy.Count) domains do not have a malware filter policy.`n`n") + $null = $Result.Append("**Domains Without Policy:**`n`n") foreach ($Domain in $DomainsWithoutPolicy) { - $Result += "- $Domain`n" + $null = $Result.Append("- $Domain`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA233.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA233.ps1 index 30484096a341..853732211588 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA233.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA233.ps1 @@ -34,14 +34,14 @@ function Invoke-CippTestORCA233 { if ($NonCompliantDomains.Count -eq 0) { $Status = 'Passed' - $Result = "All domains are properly configured for mail flow.`n`n" - $Result += "**Compliant Domains:** $($CompliantDomains.Count)" + $Result = [System.Text.StringBuilder]::new("All domains are properly configured for mail flow.`n`n") + $null = $Result.Append("**Compliant Domains:** $($CompliantDomains.Count)") } else { $Status = 'Failed' - $Result = "$($NonCompliantDomains.Count) domains may not be properly configured for mail flow.`n`n" - $Result += "**Domains Needing Review:**`n`n" + $Result = [System.Text.StringBuilder]::new("$($NonCompliantDomains.Count) domains may not be properly configured for mail flow.`n`n") + $null = $Result.Append("**Domains Needing Review:**`n`n") foreach ($Domain in $NonCompliantDomains) { - $Result += "- $Domain`n" + $null = $Result.Append("- $Domain`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA233_1.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA233_1.ps1 index 4cd287b564f9..3fb1c2970d87 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA233_1.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA233_1.ps1 @@ -26,12 +26,12 @@ function Invoke-CippTestORCA233_1 { if ($EnhancedFilteringEnabled) { $Status = 'Passed' - $Result = "Enhanced filtering appears to be properly configured.`n`n" - $Result += "**Configuration:** Reviewed" + $Result = [System.Text.StringBuilder]::new("Enhanced filtering appears to be properly configured.`n`n") + $null = $Result.Append("**Configuration:** Reviewed") } else { $Status = 'Informational' - $Result = "Unable to fully determine enhanced filtering status. Manual review recommended.`n`n" - $Result += "**Action Required:** Review inbound connectors for enhanced filtering configuration" + $Result = [System.Text.StringBuilder]::new("Unable to fully determine enhanced filtering status. Manual review recommended.`n`n") + $null = $Result.Append("**Action Required:** Review inbound connectors for enhanced filtering configuration") } Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA233_1' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Enhanced filtering on default connectors' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Configuration' diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA234.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA234.ps1 index 7b6c7e3abf74..a5ec89530341 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA234.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA234.ps1 @@ -17,12 +17,12 @@ function Invoke-CippTestORCA234 { if ($Policy.AllowSafeDocsOpen -eq $false) { $Status = 'Passed' - $Result = "Click through is disabled for Safe Documents.`n`n" - $Result += "**AllowSafeDocsOpen:** $($Policy.AllowSafeDocsOpen)" + $Result = [System.Text.StringBuilder]::new("Click through is disabled for Safe Documents.`n`n") + $null = $Result.Append("**AllowSafeDocsOpen:** $($Policy.AllowSafeDocsOpen)") } else { $Status = 'Failed' - $Result = "Click through is enabled for Safe Documents.`n`n" - $Result += "**AllowSafeDocsOpen:** $($Policy.AllowSafeDocsOpen)" + $Result = [System.Text.StringBuilder]::new("Click through is enabled for Safe Documents.`n`n") + $null = $Result.Append("**AllowSafeDocsOpen:** $($Policy.AllowSafeDocsOpen)") } Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA234' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Click through is disabled for Safe Documents' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Safe Attachments' diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA235.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA235.ps1 index db2406d9e9d8..ae123a59de2c 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA235.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA235.ps1 @@ -20,17 +20,17 @@ function Invoke-CippTestORCA235 { if ($CustomDomains.Count -eq 0) { $Status = 'Passed' - $Result = "No custom domains found. Only using onmicrosoft.com domain.`n`n" - $Result += "**Total Domains:** $($AcceptedDomains.Count)" + $Result = [System.Text.StringBuilder]::new("No custom domains found. Only using onmicrosoft.com domain.`n`n") + $null = $Result.Append("**Total Domains:** $($AcceptedDomains.Count)") } else { $Status = 'Informational' - $Result = "Found $($CustomDomains.Count) custom domains that should have SPF records configured.`n`n" - $Result += "**Custom Domains:**`n`n" + $Result = [System.Text.StringBuilder]::new("Found $($CustomDomains.Count) custom domains that should have SPF records configured.`n`n") + $null = $Result.Append("**Custom Domains:**`n`n") foreach ($Domain in $CustomDomains) { - $Result += "- $($Domain.DomainName)`n" + $null = $Result.Append("- $($Domain.DomainName)`n") } - $Result += "`n**Action Required:** Verify that each custom domain has an SPF record including Microsoft 365:`n" - $Result += "``v=spf1 include:spf.protection.outlook.com -all``" + $null = $Result.Append("`n**Action Required:** Verify that each custom domain has an SPF record including Microsoft 365:`n") + $null = $Result.Append("``v=spf1 include:spf.protection.outlook.com -all``") } Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA235' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'SPF records setup for custom domains' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Configuration' diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA236.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA236.ps1 index 36bdc40b63a1..290b843d81af 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA236.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA236.ps1 @@ -26,16 +26,16 @@ function Invoke-CippTestORCA236 { if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = "All Safe Links policies have email protection enabled.`n`n" - $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + $Result = [System.Text.StringBuilder]::new("All Safe Links policies have email protection enabled.`n`n") + $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)") } else { $Status = 'Failed' - $Result = "$($FailedPolicies.Count) Safe Links policies do not have email protection enabled.`n`n" - $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" - $Result += "| Policy Name | Enable Safe Links For Email |`n" - $Result += "|------------|----------------------------|`n" + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) Safe Links policies do not have email protection enabled.`n`n") + $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") + $null = $Result.Append("| Policy Name | Enable Safe Links For Email |`n") + $null = $Result.Append("|------------|----------------------------|`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.Identity) | $($Policy.EnableSafeLinksForEmail) |`n" + $null = $Result.Append("| $($Policy.Identity) | $($Policy.EnableSafeLinksForEmail) |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA237.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA237.ps1 index 37bbc9ba6f15..03d259bde938 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA237.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA237.ps1 @@ -26,16 +26,16 @@ function Invoke-CippTestORCA237 { if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = "All Safe Links policies have Teams protection enabled.`n`n" - $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + $Result = [System.Text.StringBuilder]::new("All Safe Links policies have Teams protection enabled.`n`n") + $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)") } else { $Status = 'Failed' - $Result = "$($FailedPolicies.Count) Safe Links policies do not have Teams protection enabled.`n`n" - $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" - $Result += "| Policy Name | Enable Safe Links For Teams |`n" - $Result += "|------------|----------------------------|`n" + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) Safe Links policies do not have Teams protection enabled.`n`n") + $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") + $null = $Result.Append("| Policy Name | Enable Safe Links For Teams |`n") + $null = $Result.Append("|------------|----------------------------|`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.Identity) | $($Policy.EnableSafeLinksForTeams) |`n" + $null = $Result.Append("| $($Policy.Identity) | $($Policy.EnableSafeLinksForTeams) |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA238.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA238.ps1 index 53c85ab0955f..c93892a42786 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA238.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA238.ps1 @@ -26,16 +26,16 @@ function Invoke-CippTestORCA238 { if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = "All Safe Links policies have Office document protection enabled.`n`n" - $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + $Result = [System.Text.StringBuilder]::new("All Safe Links policies have Office document protection enabled.`n`n") + $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)") } else { $Status = 'Failed' - $Result = "$($FailedPolicies.Count) Safe Links policies do not have Office document protection enabled.`n`n" - $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" - $Result += "| Policy Name | Enable Safe Links For Office |`n" - $Result += "|------------|------------------------------|`n" + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) Safe Links policies do not have Office document protection enabled.`n`n") + $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") + $null = $Result.Append("| Policy Name | Enable Safe Links For Office |`n") + $null = $Result.Append("|------------|------------------------------|`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.Identity) | $($Policy.EnableSafeLinksForOffice) |`n" + $null = $Result.Append("| $($Policy.Identity) | $($Policy.EnableSafeLinksForOffice) |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA239.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA239.ps1 index 629181cb5c95..414b918df87f 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA239.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA239.ps1 @@ -63,13 +63,13 @@ function Invoke-CippTestORCA239 { if ($Issues.Count -eq 0) { $Status = 'Passed' - $Result = "No exclusions found in built-in protection policies." + $Result = [System.Text.StringBuilder]::new("No exclusions found in built-in protection policies.") } else { $Status = 'Failed' - $Result = "Found $($Issues.Count) policies with exclusions that bypass built-in protection.`n`n" - $Result += "**Issues Found:**`n`n" + $Result = [System.Text.StringBuilder]::new("Found $($Issues.Count) policies with exclusions that bypass built-in protection.`n`n") + $null = $Result.Append("**Issues Found:**`n`n") foreach ($Issue in $Issues) { - $Result += "- $Issue`n" + $null = $Result.Append("- $Issue`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA240.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA240.ps1 index 746b76c06af1..ee20556e366d 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA240.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA240.ps1 @@ -17,12 +17,12 @@ function Invoke-CippTestORCA240 { if ($Config.ExternalInOutlook -ne 'Disabled') { $Status = 'Passed' - $Result = "Outlook external tags are configured.`n`n" - $Result += "**ExternalInOutlook:** $($Config.ExternalInOutlook)" + $Result = [System.Text.StringBuilder]::new("Outlook external tags are configured.`n`n") + $null = $Result.Append("**ExternalInOutlook:** $($Config.ExternalInOutlook)") } else { $Status = 'Failed' - $Result = "Outlook external tags are NOT configured.`n`n" - $Result += "**ExternalInOutlook:** $($Config.ExternalInOutlook)" + $Result = [System.Text.StringBuilder]::new("Outlook external tags are NOT configured.`n`n") + $null = $Result.Append("**ExternalInOutlook:** $($Config.ExternalInOutlook)") } Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA240' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Outlook external tags are configured' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Configuration' diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA241.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA241.ps1 index 6b53171fe109..e01a95a886c1 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA241.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA241.ps1 @@ -26,16 +26,16 @@ function Invoke-CippTestORCA241 { if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = "All anti-phishing policies have First Contact Safety Tips enabled.`n`n" - $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + $Result = [System.Text.StringBuilder]::new("All anti-phishing policies have First Contact Safety Tips enabled.`n`n") + $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)") } else { $Status = 'Failed' - $Result = "$($FailedPolicies.Count) anti-phishing policies do not have First Contact Safety Tips enabled.`n`n" - $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" - $Result += "| Policy Name | Enable First Contact Safety Tips |`n" - $Result += "|------------|----------------------------------|`n" + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) anti-phishing policies do not have First Contact Safety Tips enabled.`n`n") + $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") + $null = $Result.Append("| Policy Name | Enable First Contact Safety Tips |`n") + $null = $Result.Append("|------------|----------------------------------|`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.Identity) | $($Policy.EnableFirstContactSafetyTips) |`n" + $null = $Result.Append("| $($Policy.Identity) | $($Policy.EnableFirstContactSafetyTips) |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA242.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA242.ps1 index ba691ddf939e..b2e15b06483a 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA242.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA242.ps1 @@ -10,15 +10,15 @@ function Invoke-CippTestORCA242 { # Since we don't have an alert policy cache, we'll provide informational guidance $Status = 'Informational' - $Result = "Alert policies for protection features should be enabled and monitored.`n`n" - $Result += "**Recommended Alert Policies:**`n`n" - $Result += "- Messages reported by users as malware or phish`n" - $Result += "- Email sending limit exceeded`n" - $Result += "- Suspicious email forwarding activity`n" - $Result += "- Malware campaign detected`n" - $Result += "- Suspicious connector activity`n" - $Result += "- Unusual external user file activity`n" - $Result += "`n**Action Required:** Verify alert policies are configured in Microsoft 365 Security & Compliance Center" + $Result = [System.Text.StringBuilder]::new("Alert policies for protection features should be enabled and monitored.`n`n") + $null = $Result.Append("**Recommended Alert Policies:**`n`n") + $null = $Result.Append("- Messages reported by users as malware or phish`n") + $null = $Result.Append("- Email sending limit exceeded`n") + $null = $Result.Append("- Suspicious email forwarding activity`n") + $null = $Result.Append("- Malware campaign detected`n") + $null = $Result.Append("- Suspicious connector activity`n") + $null = $Result.Append("- Unusual external user file activity`n") + $null = $Result.Append("`n**Action Required:** Verify alert policies are configured in Microsoft 365 Security & Compliance Center") Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA242' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Important protection alerts enabled' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Configuration' diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA243.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA243.ps1 index 18e8f8ceb02f..7f6fac60c6ed 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA243.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA243.ps1 @@ -18,16 +18,16 @@ function Invoke-CippTestORCA243 { if ($NonAuthDomains.Count -eq 0) { $Status = 'Passed' - $Result = "All domains are authoritative. No inbound connectors needed.`n`n" - $Result += "**Total Domains:** $($AcceptedDomains.Count)" + $Result = [System.Text.StringBuilder]::new("All domains are authoritative. No inbound connectors needed.`n`n") + $null = $Result.Append("**Total Domains:** $($AcceptedDomains.Count)") } else { $Status = 'Informational' - $Result = "Found $($NonAuthDomains.Count) non-authoritative domains.`n`n" - $Result += "**Domains Requiring Inbound Connectors:**`n`n" + $Result = [System.Text.StringBuilder]::new("Found $($NonAuthDomains.Count) non-authoritative domains.`n`n") + $null = $Result.Append("**Domains Requiring Inbound Connectors:**`n`n") foreach ($Domain in $NonAuthDomains) { - $Result += "- $($Domain.DomainName) (Type: $($Domain.DomainType))`n" + $null = $Result.Append("- $($Domain.DomainName) (Type: $($Domain.DomainType))`n") } - $Result += "`n**Action Required:** Verify inbound connectors are configured with proper authentication for these domains" + $null = $Result.Append("`n**Action Required:** Verify inbound connectors are configured with proper authentication for these domains") } Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA243' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Authenticated Receive Chain for non-EOP domains' -UserImpact 'Medium' -ImplementationEffort 'High' -Category 'Configuration' diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA244.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA244.ps1 index 7c08c0afce41..7b413894a8a4 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA244.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA244.ps1 @@ -26,16 +26,16 @@ function Invoke-CippTestORCA244 { if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = "All anti-spam policies honor sending domain DMARC.`n`n" - $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + $Result = [System.Text.StringBuilder]::new("All anti-spam policies honor sending domain DMARC.`n`n") + $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)") } else { $Status = 'Failed' - $Result = "$($FailedPolicies.Count) anti-spam policies do not honor sending domain DMARC.`n`n" - $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" - $Result += "| Policy Name | Honor DMARC Policy |`n" - $Result += "|------------|--------------------|`n" + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) anti-spam policies do not honor sending domain DMARC.`n`n") + $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") + $null = $Result.Append("| Policy Name | Honor DMARC Policy |`n") + $null = $Result.Append("|------------|--------------------|`n") foreach ($Policy in $FailedPolicies) { - $Result += "| $($Policy.Identity) | $($Policy.HonorDMARCPolicy) |`n" + $null = $Result.Append("| $($Policy.Identity) | $($Policy.HonorDMARCPolicy) |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24541.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24541.ps1 index 72b65a2d6f21..e3142271da5b 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24541.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24541.ps1 @@ -23,18 +23,18 @@ function Invoke-CippTestZTNA24541 { $Passed = $AssignedPolicies.Count -gt 0 if ($Passed) { - $ResultMarkdown = "✅ At least one Windows compliance policy exists and is assigned.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("✅ At least one Windows compliance policy exists and is assigned.`n`n") } else { - $ResultMarkdown = "❌ No Windows compliance policy exists or none are assigned.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("❌ No Windows compliance policy exists or none are assigned.`n`n") } - $ResultMarkdown += "## Windows Compliance Policies`n`n" - $ResultMarkdown += "| Policy Name | Assigned |`n" - $ResultMarkdown += "| :---------- | :------- |`n" + $null = $ResultMarkdown.Append("## Windows Compliance Policies`n`n") + $null = $ResultMarkdown.Append("| Policy Name | Assigned |`n") + $null = $ResultMarkdown.Append("| :---------- | :------- |`n") foreach ($policy in $WindowsPolicies) { $assigned = if ($policy.assignments -and $policy.assignments.Count -gt 0) { '✅ Yes' } else { '❌ No' } - $ResultMarkdown += "| $($policy.displayName) | $assigned |`n" + $null = $ResultMarkdown.Append("| $($policy.displayName) | $assigned |`n") } $Status = if ($Passed) { 'Passed' } else { 'Failed' } diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24542.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24542.ps1 index 1fb548edd6e8..7a750097f195 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24542.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24542.ps1 @@ -20,18 +20,18 @@ function Invoke-CippTestZTNA24542 { $Passed = $AssignedPolicies.Count -gt 0 if ($Passed) { - $ResultMarkdown = "✅ At least one macOS compliance policy exists and is assigned.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("✅ At least one macOS compliance policy exists and is assigned.`n`n") } else { - $ResultMarkdown = "❌ No macOS compliance policy exists or none are assigned.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("❌ No macOS compliance policy exists or none are assigned.`n`n") } - $ResultMarkdown += "## macOS Compliance Policies`n`n" - $ResultMarkdown += "| Policy Name | Assigned |`n" - $ResultMarkdown += "| :---------- | :------- |`n" + $null = $ResultMarkdown.Append("## macOS Compliance Policies`n`n") + $null = $ResultMarkdown.Append("| Policy Name | Assigned |`n") + $null = $ResultMarkdown.Append("| :---------- | :------- |`n") foreach ($policy in $MacOSPolicies) { $assigned = if ($policy.assignments -and $policy.assignments.Count -gt 0) { '✅ Yes' } else { '❌ No' } - $ResultMarkdown += "| $($policy.displayName) | $assigned |`n" + $null = $ResultMarkdown.Append("| $($policy.displayName) | $assigned |`n") } $Status = if ($Passed) { 'Passed' } else { 'Failed' } diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24543.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24543.ps1 index 06a3802e8724..c4bb08d74a22 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24543.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24543.ps1 @@ -21,18 +21,18 @@ function Invoke-CippTestZTNA24543 { $Passed = $AssignedPolicies.Count -gt 0 if ($Passed) { - $ResultMarkdown = "✅ At least one iOS/iPadOS compliance policy exists and is assigned.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("✅ At least one iOS/iPadOS compliance policy exists and is assigned.`n`n") } else { - $ResultMarkdown = "❌ No iOS/iPadOS compliance policy exists or none are assigned.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("❌ No iOS/iPadOS compliance policy exists or none are assigned.`n`n") } - $ResultMarkdown += "## iOS/iPadOS Compliance Policies`n`n" - $ResultMarkdown += "| Policy Name | Assigned |`n" - $ResultMarkdown += "| :---------- | :------- |`n" + $null = $ResultMarkdown.Append("## iOS/iPadOS Compliance Policies`n`n") + $null = $ResultMarkdown.Append("| Policy Name | Assigned |`n") + $null = $ResultMarkdown.Append("| :---------- | :------- |`n") foreach ($policy in $iOSPolicies) { $assigned = if ($policy.assignments -and $policy.assignments.Count -gt 0) { '✅ Yes' } else { '❌ No' } - $ResultMarkdown += "| $($policy.displayName) | $assigned |`n" + $null = $ResultMarkdown.Append("| $($policy.displayName) | $assigned |`n") } $Status = if ($Passed) { 'Passed' } else { 'Failed' } diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24545.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24545.ps1 index bdbc75e5a84e..c490b477c98e 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24545.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24545.ps1 @@ -22,18 +22,18 @@ function Invoke-CippTestZTNA24545 { $Passed = $AssignedPolicies.Count -gt 0 if ($Passed) { - $ResultMarkdown = "✅ At least one compliance policy for Android Enterprise Fully managed devices exists and is assigned.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("✅ At least one compliance policy for Android Enterprise Fully managed devices exists and is assigned.`n`n") } else { - $ResultMarkdown = "❌ No compliance policy for Android Enterprise exists or none are assigned.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("❌ No compliance policy for Android Enterprise exists or none are assigned.`n`n") } - $ResultMarkdown += "## Android Device Owner Compliance Policies`n`n" - $ResultMarkdown += "| Policy Name | Assigned |`n" - $ResultMarkdown += "| :---------- | :------- |`n" + $null = $ResultMarkdown.Append("## Android Device Owner Compliance Policies`n`n") + $null = $ResultMarkdown.Append("| Policy Name | Assigned |`n") + $null = $ResultMarkdown.Append("| :---------- | :------- |`n") foreach ($policy in $AndroidPolicies) { $assigned = if ($policy.assignments -and $policy.assignments.Count -gt 0) { '✅ Yes' } else { '❌ No' } - $ResultMarkdown += "| $($policy.displayName) | $assigned |`n" + $null = $ResultMarkdown.Append("| $($policy.displayName) | $assigned |`n") } $Status = if ($Passed) { 'Passed' } else { 'Failed' } diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24547.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24547.ps1 index b7f2ace76377..38ceb556ca60 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24547.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24547.ps1 @@ -22,18 +22,18 @@ function Invoke-CippTestZTNA24547 { $Passed = $AssignedPolicies.Count -gt 0 if ($Passed) { - $ResultMarkdown = "✅ At least one compliance policy for Android Work Profile devices exists and is assigned.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("✅ At least one compliance policy for Android Work Profile devices exists and is assigned.`n`n") } else { - $ResultMarkdown = "❌ No compliance policy for Android Work Profile exists or none are assigned.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("❌ No compliance policy for Android Work Profile exists or none are assigned.`n`n") } - $ResultMarkdown += "## Android Work Profile Compliance Policies`n`n" - $ResultMarkdown += "| Policy Name | Assigned |`n" - $ResultMarkdown += "| :---------- | :------- |`n" + $null = $ResultMarkdown.Append("## Android Work Profile Compliance Policies`n`n") + $null = $ResultMarkdown.Append("| Policy Name | Assigned |`n") + $null = $ResultMarkdown.Append("| :---------- | :------- |`n") foreach ($policy in $AndroidPolicies) { $assigned = if ($policy.assignments -and $policy.assignments.Count -gt 0) { '✅ Yes' } else { '❌ No' } - $ResultMarkdown += "| $($policy.displayName) | $assigned |`n" + $null = $ResultMarkdown.Append("| $($policy.displayName) | $assigned |`n") } $Status = if ($Passed) { 'Passed' } else { 'Failed' } diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24548.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24548.ps1 index 0d5e586f0f67..0809843b6969 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24548.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24548.ps1 @@ -20,18 +20,18 @@ function Invoke-CippTestZTNA24548 { $Passed = $AssignedPolicies.Count -gt 0 if ($Passed) { - $ResultMarkdown = "✅ At least one iOS app protection policy exists and is assigned.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("✅ At least one iOS app protection policy exists and is assigned.`n`n") } else { - $ResultMarkdown = "❌ No iOS app protection policy exists or none are assigned.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("❌ No iOS app protection policy exists or none are assigned.`n`n") } - $ResultMarkdown += "## iOS App Protection Policies`n`n" - $ResultMarkdown += "| Policy Name | Assigned |`n" - $ResultMarkdown += "| :---------- | :------- |`n" + $null = $ResultMarkdown.Append("## iOS App Protection Policies`n`n") + $null = $ResultMarkdown.Append("| Policy Name | Assigned |`n") + $null = $ResultMarkdown.Append("| :---------- | :------- |`n") foreach ($policy in $IosPolicies) { $assigned = if ($policy.assignments -and $policy.assignments.Count -gt 0) { '✅ Yes' } else { '❌ No' } - $ResultMarkdown += "| $($policy.displayName) | $assigned |`n" + $null = $ResultMarkdown.Append("| $($policy.displayName) | $assigned |`n") } $Status = if ($Passed) { 'Passed' } else { 'Failed' } diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24549.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24549.ps1 index 0b8c6887875f..2837b609f04d 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24549.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24549.ps1 @@ -20,18 +20,18 @@ function Invoke-CippTestZTNA24549 { $Passed = $AssignedPolicies.Count -gt 0 if ($Passed) { - $ResultMarkdown = "✅ At least one Android app protection policy exists and is assigned.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("✅ At least one Android app protection policy exists and is assigned.`n`n") } else { - $ResultMarkdown = "❌ No Android app protection policy exists or none are assigned.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("❌ No Android app protection policy exists or none are assigned.`n`n") } - $ResultMarkdown += "## Android App Protection Policies`n`n" - $ResultMarkdown += "| Policy Name | Assigned |`n" - $ResultMarkdown += "| :---------- | :------- |`n" + $null = $ResultMarkdown.Append("## Android App Protection Policies`n`n") + $null = $ResultMarkdown.Append("| Policy Name | Assigned |`n") + $null = $ResultMarkdown.Append("| :---------- | :------- |`n") foreach ($policy in $AndroidPolicies) { $assigned = if ($policy.assignments -and $policy.assignments.Count -gt 0) { '✅ Yes' } else { '❌ No' } - $ResultMarkdown += "| $($policy.displayName) | $assigned |`n" + $null = $ResultMarkdown.Append("| $($policy.displayName) | $assigned |`n") } $Status = if ($Passed) { 'Passed' } else { 'Failed' } diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24553.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24553.ps1 index cb9586dcf3c5..859ecdf83c60 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24553.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24553.ps1 @@ -27,19 +27,19 @@ function Invoke-CippTestZTNA24553 { $Passed = $AssignedPolicies.Count -gt 0 if ($Passed) { - $ResultMarkdown = "✅ Windows Update policies are configured and assigned.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("✅ Windows Update policies are configured and assigned.`n`n") } else { - $ResultMarkdown = "❌ No Windows Update policies are configured or assigned.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("❌ No Windows Update policies are configured or assigned.`n`n") } - $ResultMarkdown += "## Windows Update Policies`n`n" - $ResultMarkdown += "| Policy Name | Type | Assigned |`n" - $ResultMarkdown += "| :---------- | :--- | :------- |`n" + $null = $ResultMarkdown.Append("## Windows Update Policies`n`n") + $null = $ResultMarkdown.Append("| Policy Name | Type | Assigned |`n") + $null = $ResultMarkdown.Append("| :---------- | :--- | :------- |`n") foreach ($policy in $UpdatePolicies) { $type = if ($policy.'@odata.type' -eq '#microsoft.graph.windowsUpdateForBusinessConfiguration') { 'Update' } else { 'Compliance' } $assigned = if ($policy.assignments -and $policy.assignments.Count -gt 0) { '✅ Yes' } else { '❌ No' } - $ResultMarkdown += "| $($policy.displayName) | $type | $assigned |`n" + $null = $ResultMarkdown.Append("| $($policy.displayName) | $type | $assigned |`n") } $Status = if ($Passed) { 'Passed' } else { 'Failed' } diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24569.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24569.ps1 index 244fff8ecaa3..544ebe43eaed 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24569.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24569.ps1 @@ -25,23 +25,23 @@ function Invoke-CippTestZTNA24569 { $Passed = $AssignedFileVaultPolicies.Count -gt 0 if ($Passed) { - $ResultMarkdown = "✅ macOS FileVault encryption policies are configured and assigned in Intune.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("✅ macOS FileVault encryption policies are configured and assigned in Intune.`n`n") } else { - $ResultMarkdown = "❌ No relevant macOS FileVault encryption policies are configured or assigned.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("❌ No relevant macOS FileVault encryption policies are configured or assigned.`n`n") } if ($FileVaultEnabledPolicies.Count -gt 0) { - $ResultMarkdown += "## macOS FileVault Policies`n`n" - $ResultMarkdown += "| Policy Name | FileVault Enabled | Assigned |`n" - $ResultMarkdown += "| :---------- | :---------------- | :------- |`n" + $null = $ResultMarkdown.Append("## macOS FileVault Policies`n`n") + $null = $ResultMarkdown.Append("| Policy Name | FileVault Enabled | Assigned |`n") + $null = $ResultMarkdown.Append("| :---------- | :---------------- | :------- |`n") foreach ($policy in $FileVaultEnabledPolicies) { $fileVault = if ($policy.fileVaultEnabled -eq $true) { '✅ Yes' } else { '❌ No' } $assigned = if ($policy.assignments -and $policy.assignments.Count -gt 0) { '✅ Yes' } else { '❌ No' } - $ResultMarkdown += "| $($policy.displayName) | $fileVault | $assigned |`n" + $null = $ResultMarkdown.Append("| $($policy.displayName) | $fileVault | $assigned |`n") } } else { - $ResultMarkdown += "No macOS Endpoint Protection policies with FileVault settings found.`n" + $null = $ResultMarkdown.Append("No macOS Endpoint Protection policies with FileVault settings found.`n") } $Status = if ($Passed) { 'Passed' } else { 'Failed' } diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24576.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24576.ps1 index a513cf5f40ce..a767d7884d42 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24576.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24576.ps1 @@ -24,22 +24,22 @@ function Invoke-CippTestZTNA24576 { $Passed = $AssignedPolicies.Count -gt 0 if ($Passed) { - $ResultMarkdown = "✅ An Endpoint analytics policy is created and assigned.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("✅ An Endpoint analytics policy is created and assigned.`n`n") } else { - $ResultMarkdown = "❌ Endpoint analytics policy is not created or not assigned.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("❌ Endpoint analytics policy is not created or not assigned.`n`n") } if ($WindowsHealthMonitoringPolicies.Count -gt 0) { - $ResultMarkdown += "## Endpoint Analytics Policies`n`n" - $ResultMarkdown += "| Policy Name | Assigned |`n" - $ResultMarkdown += "| :---------- | :------- |`n" + $null = $ResultMarkdown.Append("## Endpoint Analytics Policies`n`n") + $null = $ResultMarkdown.Append("| Policy Name | Assigned |`n") + $null = $ResultMarkdown.Append("| :---------- | :------- |`n") foreach ($policy in $WindowsHealthMonitoringPolicies) { $assigned = if ($policy.assignments -and $policy.assignments.Count -gt 0) { '✅ Yes' } else { '❌ No' } - $ResultMarkdown += "| $($policy.displayName) | $assigned |`n" + $null = $ResultMarkdown.Append("| $($policy.displayName) | $assigned |`n") } } else { - $ResultMarkdown += "No Endpoint Analytics policies found in this tenant.`n" + $null = $ResultMarkdown.Append("No Endpoint Analytics policies found in this tenant.`n") } $Status = if ($Passed) { 'Passed' } else { 'Failed' } diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24839.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24839.ps1 index e3e9757e12cc..b7feb312655c 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24839.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24839.ps1 @@ -22,23 +22,23 @@ function Invoke-CippTestZTNA24839 { $Passed = $AssignedCompliantProfiles.Count -gt 0 if ($Passed) { - $ResultMarkdown = "✅ At least one Enterprise Wi-Fi profile for iOS exists and is assigned.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("✅ At least one Enterprise Wi-Fi profile for iOS exists and is assigned.`n`n") } else { - $ResultMarkdown = "❌ No Enterprise Wi-Fi profile for iOS exists or none are assigned.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("❌ No Enterprise Wi-Fi profile for iOS exists or none are assigned.`n`n") } if ($iOSWifiConfProfiles.Count -gt 0) { - $ResultMarkdown += "## iOS WiFi Configuration Profiles`n`n" - $ResultMarkdown += "| Policy Name | Wi-Fi Security Type | Assigned |`n" - $ResultMarkdown += "| :---------- | :------------------ | :------- |`n" + $null = $ResultMarkdown.Append("## iOS WiFi Configuration Profiles`n`n") + $null = $ResultMarkdown.Append("| Policy Name | Wi-Fi Security Type | Assigned |`n") + $null = $ResultMarkdown.Append("| :---------- | :------------------ | :------- |`n") foreach ($policy in $iOSWifiConfProfiles) { $securityType = if ($policy.wiFiSecurityType) { $policy.wiFiSecurityType } else { 'Unknown' } $assigned = if ($policy.assignments -and $policy.assignments.Count -gt 0) { '✅ Yes' } else { '❌ No' } - $ResultMarkdown += "| $($policy.displayName) | $securityType | $assigned |`n" + $null = $ResultMarkdown.Append("| $($policy.displayName) | $securityType | $assigned |`n") } } else { - $ResultMarkdown += "No iOS WiFi configuration profiles found.`n" + $null = $ResultMarkdown.Append("No iOS WiFi configuration profiles found.`n") } $Status = if ($Passed) { 'Passed' } else { 'Failed' } diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24840.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24840.ps1 index 4175cdf68936..8b9f5db74108 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24840.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24840.ps1 @@ -23,23 +23,23 @@ function Invoke-CippTestZTNA24840 { $Passed = $AssignedCompliantProfiles.Count -gt 0 if ($Passed) { - $ResultMarkdown = "✅ At least one Enterprise Wi-Fi profile for android exists and is assigned.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("✅ At least one Enterprise Wi-Fi profile for android exists and is assigned.`n`n") } else { - $ResultMarkdown = "❌ No Enterprise Wi-Fi profile for android exists or none are assigned.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("❌ No Enterprise Wi-Fi profile for android exists or none are assigned.`n`n") } if ($CompliantAndroidWifiConfProfiles.Count -gt 0) { - $ResultMarkdown += "## Android Wi-Fi Configuration Profiles`n`n" - $ResultMarkdown += "| Policy Name | Wi-Fi Security Type | Assigned |`n" - $ResultMarkdown += "| :---------- | :------------------ | :------- |`n" + $null = $ResultMarkdown.Append("## Android Wi-Fi Configuration Profiles`n`n") + $null = $ResultMarkdown.Append("| Policy Name | Wi-Fi Security Type | Assigned |`n") + $null = $ResultMarkdown.Append("| :---------- | :------------------ | :------- |`n") foreach ($policy in $CompliantAndroidWifiConfProfiles) { $securityType = if ($policy.wiFiSecurityType) { $policy.wiFiSecurityType } else { 'Unknown' } $assigned = if ($policy.assignments -and $policy.assignments.Count -gt 0) { '✅ Yes' } else { '❌ No' } - $ResultMarkdown += "| $($policy.displayName) | $securityType | $assigned |`n" + $null = $ResultMarkdown.Append("| $($policy.displayName) | $securityType | $assigned |`n") } } else { - $ResultMarkdown += "No compliant Android Enterprise WiFi configuration profiles found.`n" + $null = $ResultMarkdown.Append("No compliant Android Enterprise WiFi configuration profiles found.`n") } $Status = if ($Passed) { 'Passed' } else { 'Failed' } diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24870.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24870.ps1 index 3b3fb2c65249..40ac0201df45 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24870.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24870.ps1 @@ -22,23 +22,23 @@ function Invoke-CippTestZTNA24870 { $Passed = $AssignedCompliantProfiles.Count -gt 0 if ($Passed) { - $ResultMarkdown = "✅ At least one Enterprise Wi-Fi profile for macOS exists and is assigned.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("✅ At least one Enterprise Wi-Fi profile for macOS exists and is assigned.`n`n") } else { - $ResultMarkdown = "❌ No Enterprise Wi-Fi profile for macOS exists or none are assigned.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("❌ No Enterprise Wi-Fi profile for macOS exists or none are assigned.`n`n") } if ($CompliantMacOSWifiConfProfiles.Count -gt 0) { - $ResultMarkdown += "## macOS WiFi Configuration Profiles`n`n" - $ResultMarkdown += "| Policy Name | Wi-Fi Security Type | Assigned |`n" - $ResultMarkdown += "| :---------- | :------------------ | :------- |`n" + $null = $ResultMarkdown.Append("## macOS WiFi Configuration Profiles`n`n") + $null = $ResultMarkdown.Append("| Policy Name | Wi-Fi Security Type | Assigned |`n") + $null = $ResultMarkdown.Append("| :---------- | :------------------ | :------- |`n") foreach ($policy in $CompliantMacOSWifiConfProfiles) { $securityType = if ($policy.wiFiSecurityType) { $policy.wiFiSecurityType } else { 'Unknown' } $assigned = if ($policy.assignments -and $policy.assignments.Count -gt 0) { '✅ Yes' } else { '❌ No' } - $ResultMarkdown += "| $($policy.displayName) | $securityType | $assigned |`n" + $null = $ResultMarkdown.Append("| $($policy.displayName) | $securityType | $assigned |`n") } } else { - $ResultMarkdown += "No compliant macOS Enterprise WiFi configuration profiles found.`n" + $null = $ResultMarkdown.Append("No compliant macOS Enterprise WiFi configuration profiles found.`n") } $Status = if ($Passed) { 'Passed' } else { 'Failed' } diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21797.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21797.ps1 index 64a0681cf1bc..5c82edcc4e51 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21797.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21797.ps1 @@ -82,59 +82,59 @@ function Invoke-CippTestZTNA21797 { } } - $mdInfo = "`n## Passwordless Authentication Methods allowed in tenant`n`n" + $mdInfo = [System.Text.StringBuilder]::new("`n## Passwordless Authentication Methods allowed in tenant`n`n") if ($passwordlessAuthMethods.Count -gt 0) { - $mdInfo += "| Authentication Method Name | State | Additional Info |`n" - $mdInfo += "| :------------------------ | :---- | :-------------- |`n" + $null = $mdInfo.Append("| Authentication Method Name | State | Additional Info |`n") + $null = $mdInfo.Append("| :------------------------ | :---- | :-------------- |`n") foreach ($method in $passwordlessAuthMethods) { - $mdInfo += "| $($method.Name) | $($method.State) | $($method.AdditionalInfo) |`n" + $null = $mdInfo.Append("| $($method.Name) | $($method.State) | $($method.AdditionalInfo) |`n") } } else { - $mdInfo += "No passwordless authentication methods are enabled.`n" + $null = $mdInfo.Append("No passwordless authentication methods are enabled.`n") } - $mdInfo += "`n## Conditional Access Policies targeting high risk users`n`n" + $null = $mdInfo.Append("`n## Conditional Access Policies targeting high risk users`n`n") $allEnabledHighRiskPolicies = @($caPasswordChangePolicies) + @($caBlockPolicies) if ($allEnabledHighRiskPolicies.Count -gt 0) { - $mdInfo += "| Conditional Access Policy Name | Status | Conditions |`n" - $mdInfo += "| :--------------------- | :----- | :--------- |`n" + $null = $mdInfo.Append("| Conditional Access Policy Name | Status | Conditions |`n") + $null = $mdInfo.Append("| :--------------------- | :----- | :--------- |`n") foreach ($policy in $allEnabledHighRiskPolicies) { - $conditions = 'User Risk Level: High' + $conditions = [System.Text.StringBuilder]::new('User Risk Level: High') if ($policy.grantControls.builtInControls -contains 'passwordChange') { - $conditions += ', Control: Password Change' + $null = $conditions.Append(', Control: Password Change') } if ($policy.grantControls.builtInControls -contains 'block') { - $conditions += ', Control: Block' + $null = $conditions.Append(', Control: Block') } - $mdInfo += "| $($policy.displayName) | Enabled | $conditions |`n" + $null = $mdInfo.Append("| $($policy.displayName) | Enabled | $conditions |`n") } } if ($inactiveCAPolicies.Count -gt 0) { if ($allEnabledHighRiskPolicies.Count -eq 0) { - $mdInfo += "No conditional access policies targeting high risk users found.`n`n" - $mdInfo += "### Inactive policies targeting high risk users (not contributing to security posture):`n`n" - $mdInfo += "| Conditional Access Policy Name | Status | Conditions |`n" - $mdInfo += "| :--------------------- | :----- | :--------- |`n" + $null = $mdInfo.Append("No conditional access policies targeting high risk users found.`n`n") + $null = $mdInfo.Append("### Inactive policies targeting high risk users (not contributing to security posture):`n`n") + $null = $mdInfo.Append("| Conditional Access Policy Name | Status | Conditions |`n") + $null = $mdInfo.Append("| :--------------------- | :----- | :--------- |`n") } foreach ($policy in $inactiveCAPolicies) { - $conditions = 'User Risk Level: High' + $conditions = [System.Text.StringBuilder]::new('User Risk Level: High') if ($policy.grantControls.builtInControls -contains 'passwordChange') { - $conditions += ', Control: Password Change' + $null = $conditions.Append(', Control: Password Change') } if ($policy.grantControls.builtInControls -contains 'block') { - $conditions += ', Control: Block' + $null = $conditions.Append(', Control: Block') } $status = if ($policy.state -eq 'enabledForReportingButNotEnforced') { 'Report-only' } else { 'Disabled' } - $mdInfo += "| $($policy.displayName) | $status | $conditions |`n" + $null = $mdInfo.Append("| $($policy.displayName) | $status | $conditions |`n") } } elseif ($allEnabledHighRiskPolicies.Count -eq 0) { - $mdInfo += "No conditional access policies targeting high risk users found.`n" + $null = $mdInfo.Append("No conditional access policies targeting high risk users found.`n") } $testResultMarkdown = $testResultMarkdown + $mdInfo diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21799.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21799.ps1 index 630493d373c2..98e8ceda9efd 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21799.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21799.ps1 @@ -46,9 +46,9 @@ function Invoke-CippTestZTNA21799 { $tableRows = '' if ($matchedPolicies.Count -gt 0) { - $mdInfo = "`n## $reportTitle`n`n" - $mdInfo += "| Policy Name | Grant Controls | Target Users |`n" - $mdInfo += "| :---------- | :------------- | :----------- |`n" + $mdInfo = [System.Text.StringBuilder]::new("`n## $reportTitle`n`n") + $null = $mdInfo.Append("| Policy Name | Grant Controls | Target Users |`n") + $null = $mdInfo.Append("| :---------- | :------------- | :----------- |`n") foreach ($policy in $matchedPolicies) { $grantControls = switch ($policy.grantControls) { @@ -69,7 +69,7 @@ function Invoke-CippTestZTNA21799 { $policy.conditions.users.includeUsers -join ', ' } - $mdInfo += "| $($policy.displayName) | $grantControls | $targetUsers |`n" + $null = $mdInfo.Append("| $($policy.displayName) | $grantControls | $targetUsers |`n") } } $testResultMarkdown = $testResultMarkdown + $mdInfo diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21804.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21804.ps1 index 9efb77db92a8..2882e78b0876 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21804.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21804.ps1 @@ -27,12 +27,12 @@ function Invoke-CippTestZTNA21804 { $reportTitle = 'Weak authentication methods' - $mdInfo = "`n## $reportTitle`n`n" - $mdInfo += "| Method ID | Is method weak? | State |`n" - $mdInfo += "| :-------- | :-------------- | :---- |`n" + $mdInfo = [System.Text.StringBuilder]::new("`n## $reportTitle`n`n") + $null = $mdInfo.Append("| Method ID | Is method weak? | State |`n") + $null = $mdInfo.Append("| :-------- | :-------------- | :---- |`n") foreach ($method in $matchedMethods) { - $mdInfo += "| $($method.id) | Yes | $($method.state) |`n" + $null = $mdInfo.Append("| $($method.id) | Yes | $($method.state) |`n") } $testResultMarkdown = $testResultMarkdown + $mdInfo diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21806.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21806.ps1 index 101620a0966f..cd18bb9a0cbc 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21806.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21806.ps1 @@ -33,15 +33,15 @@ function Invoke-CippTestZTNA21806 { $tableRows = '' if ($matchedPolicies.Count -gt 0) { - $mdInfo = "`n## $reportTitle`n`n" - $mdInfo += "| Policy Name | User Actions Targeted | Grant Controls Applied |`n" - $mdInfo += "| :---------- | :-------------------- | :--------------------- |`n" + $mdInfo = [System.Text.StringBuilder]::new("`n## $reportTitle`n`n") + $null = $mdInfo.Append("| Policy Name | User Actions Targeted | Grant Controls Applied |`n") + $null = $mdInfo.Append("| :---------- | :-------------------- | :--------------------- |`n") foreach ($policy in $matchedPolicies) { - $mdInfo += "| $($policy.displayName) | $($policy.conditions.applications.includeUserActions) | $($policy.grantControls.builtInControls -join ', ') |`n" + $null = $mdInfo.Append("| $($policy.displayName) | $($policy.conditions.applications.includeUserActions) | $($policy.grantControls.builtInControls -join ', ') |`n") } } else { - $mdInfo = 'No Conditional Access policies targeting security information registration.' + $mdInfo = [System.Text.StringBuilder]::new('No Conditional Access policies targeting security information registration.') } $testResultMarkdown = $testResultMarkdown + $mdInfo diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21811.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21811.ps1 index 41ac254e7ae6..3f937afc706e 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21811.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21811.ps1 @@ -44,12 +44,12 @@ function Invoke-CippTestZTNA21811 { if ($misconfiguredDomains) { $reportTitle1 = 'Domains with password expiration enabled' - $mdInfo1 = "`n## $reportTitle1`n`n" - $mdInfo1 += "| Domain Name | Password Validity Interval |`n" - $mdInfo1 += "| :---------- | :------------------------- |`n" + $mdInfo1 = [System.Text.StringBuilder]::new("`n## $reportTitle1`n`n") + $null = $mdInfo1.Append("| Domain Name | Password Validity Interval |`n") + $null = $mdInfo1.Append("| :---------- | :------------------------- |`n") foreach ($domain in $misconfiguredDomains) { - $mdInfo1 += "| $($domain.id) | $($domain.passwordValidityPeriodInDays) |`n" + $null = $mdInfo1.Append("| $($domain.id) | $($domain.passwordValidityPeriodInDays) |`n") } $testResultMarkdown = $testResultMarkdown + $mdInfo1 @@ -57,16 +57,16 @@ function Invoke-CippTestZTNA21811 { if ($misconfiguredUsers) { $reportTitle2 = 'Users with password expiration enabled' - $mdInfo2 = "`n## $reportTitle2`n`n" - $mdInfo2 += "| Display Name | User Principal Name | User Password Expiration setting | Domain Password Expiration setting |`n" - $mdInfo2 += "| :----------- | :------------------ | :------------------------------- | :--------------------------------- |`n" + $mdInfo2 = [System.Text.StringBuilder]::new("`n## $reportTitle2`n`n") + $null = $mdInfo2.Append("| Display Name | User Principal Name | User Password Expiration setting | Domain Password Expiration setting |`n") + $null = $mdInfo2.Append("| :----------- | :------------------ | :------------------------------- | :--------------------------------- |`n") foreach ($misconfiguredUser in $misconfiguredUsers) { $displayName = $misconfiguredUser.displayName $userPrincipalName = $misconfiguredUser.userPrincipalName $userPasswordExpiration = $misconfiguredUser.passwordPolicies $domainPasswordExpiration = $misconfiguredUser.DomainPasswordValidity - $mdInfo2 += "| $displayName | $userPrincipalName | $userPasswordExpiration | $domainPasswordExpiration |`n" + $null = $mdInfo2.Append("| $displayName | $userPrincipalName | $userPasswordExpiration | $domainPasswordExpiration |`n") } $testResultMarkdown = $testResultMarkdown + $mdInfo2 diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21812.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21812.ps1 index 5bd0291730a5..8b961abed443 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21812.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21812.ps1 @@ -19,16 +19,16 @@ function Invoke-CippTestZTNA21812 { $Passed = $GlobalAdmins.Count -le 5 if ($Passed) { - $ResultMarkdown = "Maximum number of Global Administrators doesn't exceed five users/service principals.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("Maximum number of Global Administrators doesn't exceed five users/service principals.`n`n") } else { - $ResultMarkdown = "Maximum number of Global Administrators exceeds five users/service principals.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("Maximum number of Global Administrators exceeds five users/service principals.`n`n") } if ($GlobalAdmins.Count -gt 0) { - $ResultMarkdown += "## Global Administrators`n`n" - $ResultMarkdown += "### Total number of Global Administrators: $($GlobalAdmins.Count)`n`n" - $ResultMarkdown += "| Display Name | Object Type | User Principal Name |`n" - $ResultMarkdown += "| :----------- | :---------- | :------------------ |`n" + $null = $ResultMarkdown.Append("## Global Administrators`n`n") + $null = $ResultMarkdown.Append("### Total number of Global Administrators: $($GlobalAdmins.Count)`n`n") + $null = $ResultMarkdown.Append("| Display Name | Object Type | User Principal Name |`n") + $null = $ResultMarkdown.Append("| :----------- | :---------- | :------------------ |`n") foreach ($GlobalAdmin in $GlobalAdmins) { $DisplayName = $GlobalAdmin.displayName @@ -45,7 +45,7 @@ function Invoke-CippTestZTNA21812 { default { 'https://entra.microsoft.com' } } - $ResultMarkdown += "| [$DisplayName]($PortalLink) | $ObjectType | $UserPrincipalName |`n" + $null = $ResultMarkdown.Append("| [$DisplayName]($PortalLink) | $ObjectType | $UserPrincipalName |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21813.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21813.ps1 index 600acbfc9ab4..9f3f82f2e05a 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21813.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21813.ps1 @@ -94,13 +94,13 @@ function Invoke-CippTestZTNA21813 { $HasHighRatio = $true } - $MdInfo = "`n## Privileged role assignment summary`n`n" - $MdInfo += "**Global administrator role count:** $GARoleAssignmentCount ($GAPercentage%) - $StatusIndicator`n`n" - $MdInfo += "**Other privileged role count:** $PrivilegedRoleAssignmentCount ($OtherPercentage%)`n`n" + $MdInfo = [System.Text.StringBuilder]::new("`n## Privileged role assignment summary`n`n") + $null = $MdInfo.Append("**Global administrator role count:** $GARoleAssignmentCount ($GAPercentage%) - $StatusIndicator`n`n") + $null = $MdInfo.Append("**Other privileged role count:** $PrivilegedRoleAssignmentCount ($OtherPercentage%)`n`n") - $MdInfo += "## User privileged role assignments`n`n" - $MdInfo += "| User | Global administrator | Other Privileged Role(s) |`n" - $MdInfo += "| :--- | :------------------- | :------ |`n" + $null = $MdInfo.Append("## User privileged role assignments`n`n") + $null = $MdInfo.Append("| User | Global administrator | Other Privileged Role(s) |`n") + $null = $MdInfo.Append("| :--- | :------------------- | :------ |`n") $SortedUsers = $UserRoleMap.Values | Sort-Object @{Expression = { -not $_.IsGA } }, @{Expression = { $_.User.displayName } } @@ -112,11 +112,11 @@ function Invoke-CippTestZTNA21813 { $RolesList = if ($OtherRoles.Count -gt 0) { ($OtherRoles -join ', ') } else { '-' } $UserLink = "https://entra.microsoft.com/#view/Microsoft_AAD_UsersAndTenants/UserProfileMenuBlade/~/AdministrativeRole/userId/$($User.id)/hidePreviewBanner~/true" - $MdInfo += "| [$($User.displayName)]($UserLink) | $IsGA | $RolesList |`n" + $null = $MdInfo.Append("| [$($User.displayName)]($UserLink) | $IsGA | $RolesList |`n") } if ($UserRoleMap.Count -eq 0) { - $MdInfo += "| No privileged users found | - | - |`n" + $null = $MdInfo.Append("| No privileged users found | - | - |`n") } if ($TotalPrivilegedRoleAssignmentCount -eq 0) { diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21814.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21814.ps1 index 3e81eafb1b68..f297f694d2da 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21814.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21814.ps1 @@ -40,15 +40,15 @@ function Invoke-CippTestZTNA21814 { $Passed = $SyncedUsers.Count -eq 0 if ($Passed) { - $ResultMarkdown = "Validated that standing or eligible privileged accounts are cloud only accounts.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("Validated that standing or eligible privileged accounts are cloud only accounts.`n`n") } else { - $ResultMarkdown = "This tenant has $($SyncedUsers.Count) privileged users that are synced from on-premise.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("This tenant has $($SyncedUsers.Count) privileged users that are synced from on-premise.`n`n") } if ($RoleData.Count -gt 0) { - $ResultMarkdown += "## Privileged Roles`n`n" - $ResultMarkdown += "| Role Name | User | Source | Status |`n" - $ResultMarkdown += "| :--- | :--- | :--- | :---: |`n" + $null = $ResultMarkdown.Append("## Privileged Roles`n`n") + $null = $ResultMarkdown.Append("| Role Name | User | Source | Status |`n") + $null = $ResultMarkdown.Append("| :--- | :--- | :--- | :---: |`n") foreach ($RoleUser in ($RoleData | Sort-Object RoleName, UserDisplayName)) { if ($RoleUser.OnPremisesSyncEnabled) { @@ -60,7 +60,7 @@ function Invoke-CippTestZTNA21814 { } $UserLink = "https://entra.microsoft.com/#view/Microsoft_AAD_UsersAndTenants/UserProfileMenuBlade/~/AdministrativeRole/userId/$($RoleUser.UserId)" - $ResultMarkdown += "| $($RoleUser.RoleName) | [$($RoleUser.UserDisplayName)]($UserLink) | $Type | $Status |`n" + $null = $ResultMarkdown.Append("| $($RoleUser.RoleName) | [$($RoleUser.UserDisplayName)]($UserLink) | $Type | $Status |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21815.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21815.ps1 index c6b7cf55ccfc..be06391c03d0 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21815.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21815.ps1 @@ -41,17 +41,17 @@ function Invoke-CippTestZTNA21815 { if ($PermanentAssignments.Count -eq 0) { $Passed = $true - $ResultMarkdown = 'No privileged users have permanent role assignments.' + $ResultMarkdown = [System.Text.StringBuilder]::new('No privileged users have permanent role assignments.') } else { $Passed = $false - $ResultMarkdown = "Privileged users with permanent role assignments were found.`n`n" - $ResultMarkdown += "## Privileged users with permanent role assignments`n`n" - $ResultMarkdown += "| User | UPN | Role Name | Assignment Type |`n" - $ResultMarkdown += "| :--- | :-- | :-------- | :-------------- |`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("Privileged users with permanent role assignments were found.`n`n") + $null = $ResultMarkdown.Append("## Privileged users with permanent role assignments`n`n") + $null = $ResultMarkdown.Append("| User | UPN | Role Name | Assignment Type |`n") + $null = $ResultMarkdown.Append("| :--- | :-- | :-------- | :-------------- |`n") foreach ($Result in $PermanentAssignments) { $PortalLink = "https://entra.microsoft.com/#view/Microsoft_AAD_UsersAndTenants/UserProfileMenuBlade/~/AdministrativeRole/userId/$($Result.PrincipalId)/hidePreviewBanner~/true" - $ResultMarkdown += "| [$($Result.PrincipalDisplayName)]($PortalLink) | $($Result.UserPrincipalName) | $($Result.RoleDisplayName) | $($Result.PrivilegeType) |`n" + $null = $ResultMarkdown.Append("| [$($Result.PrincipalDisplayName)]($PortalLink) | $($Result.UserPrincipalName) | $($Result.RoleDisplayName) | $($Result.PrivilegeType) |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21816.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21816.ps1 index d45786e122de..3705a2cb776e 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21816.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21816.ps1 @@ -24,18 +24,32 @@ function Invoke-CippTestZTNA21816 { $Users = Get-CIPPTestData -TenantFilter $Tenant -Type 'Users' $Groups = Get-CIPPTestData -TenantFilter $Tenant -Type 'Groups' - $EligibleGAs = $RoleEligibilitySchedules | Where-Object { $_.roleDefinitionId -eq $GlobalAdminRoleId } + $EligibleGAs = $RoleEligibilitySchedules.Where({ $_.roleDefinitionId -eq $GlobalAdminRoleId }) $EligibleGAUsers = 0 + # Build id-keyed lookups once to avoid O(N*M) Where-Object scans + $UsersById = @{} + foreach ($U in $Users) { $UsersById[$U.id] = $U } + $GroupsById = @{} + foreach ($G in $Groups) { $GroupsById[$G.id] = $G } + # Composite-key lookup: principalId|roleDefinitionId + $AssignmentByPrincipalRole = @{} + foreach ($A in $RoleAssignmentScheduleInstances) { + $key = '{0}|{1}' -f $A.principalId, $A.roleDefinitionId + if (-not $AssignmentByPrincipalRole.ContainsKey($key)) { + $AssignmentByPrincipalRole[$key] = $A + } + } + foreach ($EligibleGA in $EligibleGAs) { - $Principal = $Users | Where-Object { $_.id -eq $EligibleGA.principalId } | Select-Object -First 1 + $Principal = $UsersById[$EligibleGA.principalId] if ($Principal) { $EligibleGAUsers++ } else { - $GroupPrincipal = $Groups | Where-Object { $_.id -eq $EligibleGA.principalId } | Select-Object -First 1 - if ($GroupPrincipal) { - $GroupMembers = $Users | Where-Object { $_.id -in $GroupPrincipal.members } - $EligibleGAUsers = $EligibleGAUsers + $GroupMembers.Count + $GroupPrincipal = $GroupsById[$EligibleGA.principalId] + if ($GroupPrincipal -and $GroupPrincipal.members) { + $MemberSet = [System.Collections.Generic.HashSet[string]]::new([string[]]$GroupPrincipal.members) + foreach ($U in $Users) { if ($MemberSet.Contains($U.id)) { $EligibleGAUsers++ } } } } } @@ -46,9 +60,7 @@ function Invoke-CippTestZTNA21816 { $RoleMembers = Get-CippDbRoleMembers -TenantFilter $Tenant -RoleTemplateId $Role.RoletemplateId foreach ($Member in $RoleMembers) { - $Assignment = $RoleAssignmentScheduleInstances | Where-Object { - $_.principalId -eq $Member.id -and $_.roleDefinitionId -eq $Role.RoletemplateId - } | Select-Object -First 1 + $Assignment = $AssignmentByPrincipalRole['{0}|{1}' -f $Member.id, $Role.RoletemplateId] if (-not $Assignment -or ($Assignment.assignmentType -eq 'Assigned' -and $null -eq $Assignment.endDateTime)) { $MemberInfo = [PSCustomObject]@{ @@ -72,9 +84,7 @@ function Invoke-CippTestZTNA21816 { $GAMembers = Get-CippDbRoleMembers -TenantFilter $Tenant -RoleTemplateId $GlobalAdminRoleId foreach ($Member in $GAMembers) { - $Assignment = $RoleAssignmentScheduleInstances | Where-Object { - $_.principalId -eq $Member.id -and $_.roleDefinitionId -eq $GlobalAdminRoleId - } | Select-Object -First 1 + $Assignment = $AssignmentByPrincipalRole['{0}|{1}' -f $Member.id, $GlobalAdminRoleId] if (-not $Assignment -or ($Assignment.assignmentType -eq 'Assigned' -and $null -eq $Assignment.endDateTime)) { $MemberInfo = [PSCustomObject]@{ @@ -88,7 +98,7 @@ function Invoke-CippTestZTNA21816 { } if ($Member.'@odata.type' -eq '#microsoft.graph.user') { - $UserDetail = $Users | Where-Object { $_.id -eq $Member.id } | Select-Object -First 1 + $UserDetail = $UsersById[$Member.id] if ($UserDetail) { $MemberInfo.onPremisesSyncEnabled = $UserDetail.onPremisesSyncEnabled } @@ -96,10 +106,11 @@ function Invoke-CippTestZTNA21816 { } elseif ($Member.'@odata.type' -eq '#microsoft.graph.group') { $PermanentGAGroupList.Add($MemberInfo) - $Group = $Groups | Where-Object { $_.id -eq $Member.id } | Select-Object -First 1 - if ($Group) { - $GroupMembers = $Users | Where-Object { $_.id -in $Group.members } - foreach ($GroupMember in $GroupMembers) { + $Group = $GroupsById[$Member.id] + if ($Group -and $Group.members) { + $MemberSet = [System.Collections.Generic.HashSet[string]]::new([string[]]$Group.members) + foreach ($GroupMember in $Users) { + if (-not $MemberSet.Contains($GroupMember.id)) { continue } $GroupMemberInfo = [PSCustomObject]@{ displayName = $GroupMember.displayName userPrincipalName = $GroupMember.userPrincipalName @@ -123,53 +134,53 @@ function Invoke-CippTestZTNA21816 { if (-not $HasPIMUsage) { $Passed = $false - $ResultMarkdown = 'No eligible Global Administrator assignments found. PIM usage cannot be confirmed.' + $ResultMarkdown = [System.Text.StringBuilder]::new('No eligible Global Administrator assignments found. PIM usage cannot be confirmed.') } elseif ($HasNonPIMPrivileged) { $Passed = $false - $ResultMarkdown = 'Found Microsoft Entra privileged role assignments that are not managed with PIM.' + $ResultMarkdown = [System.Text.StringBuilder]::new('Found Microsoft Entra privileged role assignments that are not managed with PIM.') } elseif ($PermanentGACount -gt 2) { $Passed = $false $CustomStatus = 'Investigate' - $ResultMarkdown = 'Three or more accounts are permanently assigned the Global Administrator role. Review to determine whether these are emergency access accounts.' + $ResultMarkdown = [System.Text.StringBuilder]::new('Three or more accounts are permanently assigned the Global Administrator role. Review to determine whether these are emergency access accounts.') } else { $Passed = $true - $ResultMarkdown = 'All Microsoft Entra privileged role assignments are managed with PIM with the exception of up to two standing Global Administrator accounts.' + $ResultMarkdown = [System.Text.StringBuilder]::new('All Microsoft Entra privileged role assignments are managed with PIM with the exception of up to two standing Global Administrator accounts.') } - $ResultMarkdown += "`n`n## Assessment summary`n`n" - $ResultMarkdown += "| Metric | Count |`n" - $ResultMarkdown += "| :----- | :---- |`n" - $ResultMarkdown += "| Privileged roles found | $($PrivilegedRoles.Count) |`n" - $ResultMarkdown += "| Eligible Global Administrators | $EligibleGAUsers |`n" - $ResultMarkdown += "| Non-PIM privileged users | $($NonPIMPrivilegedUsers.Count) |`n" - $ResultMarkdown += "| Non-PIM privileged groups | $($NonPIMPrivilegedGroups.Count) |`n" - $ResultMarkdown += "| Permanent Global Administrator users | $($PermanentGAUserList.Count) |`n" + $null = $ResultMarkdown.Append("`n`n## Assessment summary`n`n") + $null = $ResultMarkdown.Append("| Metric | Count |`n") + $null = $ResultMarkdown.Append("| :----- | :---- |`n") + $null = $ResultMarkdown.Append("| Privileged roles found | $($PrivilegedRoles.Count) |`n") + $null = $ResultMarkdown.Append("| Eligible Global Administrators | $EligibleGAUsers |`n") + $null = $ResultMarkdown.Append("| Non-PIM privileged users | $($NonPIMPrivilegedUsers.Count) |`n") + $null = $ResultMarkdown.Append("| Non-PIM privileged groups | $($NonPIMPrivilegedGroups.Count) |`n") + $null = $ResultMarkdown.Append("| Permanent Global Administrator users | $($PermanentGAUserList.Count) |`n") if ($NonPIMPrivilegedUsers.Count -gt 0 -or $NonPIMPrivilegedGroups.Count -gt 0) { - $ResultMarkdown += "`n## Non-PIM managed privileged role assignments`n`n" - $ResultMarkdown += "| Display name | User principal name | Role name | Assignment type |`n" - $ResultMarkdown += "| :----------- | :------------------ | :-------- | :-------------- |`n" + $null = $ResultMarkdown.Append("`n## Non-PIM managed privileged role assignments`n`n") + $null = $ResultMarkdown.Append("| Display name | User principal name | Role name | Assignment type |`n") + $null = $ResultMarkdown.Append("| :----------- | :------------------ | :-------- | :-------------- |`n") foreach ($User in $NonPIMPrivilegedUsers) { $UserLink = "https://entra.microsoft.com/#view/Microsoft_AAD_UsersAndTenants/UserProfileMenuBlade/~/AdministrativeRole/userId/$($User.id)/hidePreviewBanner~/true" - $ResultMarkdown += "| [$($User.displayName)]($UserLink) | $($User.userPrincipalName) | $($User.roleName) | $($User.assignmentType) |`n" + $null = $ResultMarkdown.Append("| [$($User.displayName)]($UserLink) | $($User.userPrincipalName) | $($User.roleName) | $($User.assignmentType) |`n") } foreach ($Group in $NonPIMPrivilegedGroups) { $GroupLink = "https://entra.microsoft.com/#view/Microsoft_AAD_IAM/GroupDetailsMenuBlade/~/RolesAndAdministrators/groupId/$($Group.id)/menuId/" - $ResultMarkdown += "| [$($Group.displayName)]($GroupLink) | N/A (Group) | $($Group.roleName) | $($Group.assignmentType) |`n" + $null = $ResultMarkdown.Append("| [$($Group.displayName)]($GroupLink) | N/A (Group) | $($Group.roleName) | $($Group.assignmentType) |`n") } } if ($PermanentGAUserList.Count -gt 0) { - $ResultMarkdown += "`n## Permanent Global Administrator assignments`n`n" - $ResultMarkdown += "| Display name | User principal name | Assignment type | On-Premises synced |`n" - $ResultMarkdown += "| :----------- | :------------------ | :-------------- | :----------------- |`n" + $null = $ResultMarkdown.Append("`n## Permanent Global Administrator assignments`n`n") + $null = $ResultMarkdown.Append("| Display name | User principal name | Assignment type | On-Premises synced |`n") + $null = $ResultMarkdown.Append("| :----------- | :------------------ | :-------------- | :----------------- |`n") foreach ($User in $PermanentGAUserList) { $SyncStatus = if ($null -ne $User.onPremisesSyncEnabled) { $User.onPremisesSyncEnabled } else { 'N/A' } $UserLink = "https://entra.microsoft.com/#view/Microsoft_AAD_UsersAndTenants/UserProfileMenuBlade/~/AdministrativeRole/userId/$($User.id)/hidePreviewBanner~/true" - $ResultMarkdown += "| [$($User.displayName)]($UserLink) | $($User.userPrincipalName) | $($User.assignmentType) | $SyncStatus |`n" + $null = $ResultMarkdown.Append("| [$($User.displayName)]($UserLink) | $($User.userPrincipalName) | $($User.assignmentType) | $SyncStatus |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21818.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21818.ps1 index a23c36de9b9a..1ebe2fdddbc7 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21818.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21818.ps1 @@ -93,19 +93,19 @@ function Invoke-CippTestZTNA21818 { } if ($Passed) { - $ResultMarkdown = "Role notifications are properly configured for privileged role.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("Role notifications are properly configured for privileged role.`n`n") } else { - $ResultMarkdown = "Role notifications are not properly configured.`n`nNote: To save time, this check stops when it finds the first role that does not have notifications. After fixing this role and all other roles, we recommend running the check again to verify.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("Role notifications are not properly configured.`n`nNote: To save time, this check stops when it finds the first role that does not have notifications. After fixing this role and all other roles, we recommend running the check again to verify.`n`n") } - $ResultMarkdown += "## Notifications for high privileged roles`n`n" - $ResultMarkdown += "| Role Name | Notification Scenario | Notification Type | Default Recipients Enabled | Additional Recipients |`n" - $ResultMarkdown += "| :-------- | :-------------------- | :---------------- | :------------------------- | :-------------------- |`n" + $null = $ResultMarkdown.Append("## Notifications for high privileged roles`n`n") + $null = $ResultMarkdown.Append("| Role Name | Notification Scenario | Notification Type | Default Recipients Enabled | Additional Recipients |`n") + $null = $ResultMarkdown.Append("| :-------- | :-------------------- | :---------------- | :------------------------- | :-------------------- |`n") foreach ($NotificationRule in $NotificationRules) { $MatchingNotification = $Notifications | Where-Object { $_.RuleId -eq $NotificationRule.id } $Recipients = if ($NotificationRule.notificationRecipients) { ($NotificationRule.notificationRecipients -join ', ') } else { '' } - $ResultMarkdown += "| $($NotificationRule.roleDisplayName) | $($MatchingNotification.notificationScenario) | $($MatchingNotification.notificationType) | $($NotificationRule.isDefaultRecipientsEnabled) | $Recipients |`n" + $null = $ResultMarkdown.Append("| $($NotificationRule.roleDisplayName) | $($MatchingNotification.notificationScenario) | $($MatchingNotification.notificationType) | $($NotificationRule.isDefaultRecipientsEnabled) | $Recipients |`n") } $Status = if ($Passed) { 'Passed' } else { 'Failed' } diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21819.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21819.ps1 index f6f5a85e3d82..af4ca97404ec 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21819.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21819.ps1 @@ -48,13 +48,13 @@ function Invoke-CippTestZTNA21819 { } if ($Passed -eq 'Passed') { - $ResultMarkdown = "Activation alerts are configured for Global Administrator role.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("Activation alerts are configured for Global Administrator role.`n`n") } else { - $ResultMarkdown = "Activation alerts are missing or improperly configured for Global Administrator role.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("Activation alerts are missing or improperly configured for Global Administrator role.`n`n") } - $ResultMarkdown += "| Role display name | Default recipients | Additional recipients |`n" - $ResultMarkdown += "| :---------------- | :----------------- | :------------------- |`n" + $null = $ResultMarkdown.Append("| Role display name | Default recipients | Additional recipients |`n") + $null = $ResultMarkdown.Append("| :---------------- | :----------------- | :------------------- |`n") $RoleLink = 'https://entra.microsoft.com/#view/Microsoft_AAD_IAM/RolesManagementMenuBlade/~/AllRoles' $DisplayNameLink = "[$($GlobalAdminRole.displayName)]($RoleLink)" @@ -73,7 +73,7 @@ function Invoke-CippTestZTNA21819 { $Recipients } - $ResultMarkdown += "| $DisplayNameLink | $DefaultRecipientsStatus | $RecipientsDisplay |`n" + $null = $ResultMarkdown.Append("| $DisplayNameLink | $DefaultRecipientsStatus | $RecipientsDisplay |`n") Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Passed -ResultMarkdown $ResultMarkdown -Risk 'Low' -Name 'Activation alert for Global Administrator role assignment' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Privileged access' diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21820.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21820.ps1 index e1715d6d8ff6..e4afd33d4392 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21820.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21820.ps1 @@ -71,15 +71,15 @@ function Invoke-CippTestZTNA21820 { } if ($RolesWithIssues.Count -eq 0) { - $ResultMarkdown = 'Activation alerts are configured for privileged role assignments.' + $ResultMarkdown = [System.Text.StringBuilder]::new('Activation alerts are configured for privileged role assignments.') } else { - $ResultMarkdown = 'Activation alerts are missing or improperly configured for privileged roles.' + $ResultMarkdown = [System.Text.StringBuilder]::new('Activation alerts are missing or improperly configured for privileged roles.') } if ($RolesWithIssues.Count -gt 0) { - $ResultMarkdown += "`n`n## Roles with missing or misconfigured alerts`n`n" - $ResultMarkdown += "| Role display name | Default recipients | Additional recipients |`n" - $ResultMarkdown += "| :---------------- | :----------------- | :------------------- |`n" + $null = $ResultMarkdown.Append("`n`n## Roles with missing or misconfigured alerts`n`n") + $null = $ResultMarkdown.Append("| Role display name | Default recipients | Additional recipients |`n") + $null = $ResultMarkdown.Append("| :---------------- | :----------------- | :------------------- |`n") foreach ($RoleIssue in $RolesWithIssues) { $Role = $RoleIssue.Role @@ -93,9 +93,9 @@ function Invoke-CippTestZTNA21820 { } $Recipients = $RoleIssue.NotificationRecipients - $ResultMarkdown += "| $DisplayNameLink | $DefaultRecipientsStatus | $Recipients |`n" + $null = $ResultMarkdown.Append("| $DisplayNameLink | $DefaultRecipientsStatus | $Recipients |`n") } - $ResultMarkdown += "`n`n*Not all misconfigured roles may be listed. For performance reasons, this assessment stops at the first detected issue.*`n" + $null = $ResultMarkdown.Append("`n`n*Not all misconfigured roles may be listed. For performance reasons, this assessment stops at the first detected issue.*`n") } Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Passed -ResultMarkdown $ResultMarkdown -Risk 'Low' -Name 'Activation alert for all privileged role assignments' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Privileged access' diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21822.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21822.ps1 index 3fa2fa43af58..5df414fc8046 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21822.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21822.ps1 @@ -26,34 +26,34 @@ function Invoke-CippTestZTNA21822 { } if ($Passed -eq 'Passed') { - $ResultMarkdown = "Guest access is limited to approved tenants.`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("Guest access is limited to approved tenants.`n") } else { - $ResultMarkdown = "Guest access is not limited to approved tenants.`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("Guest access is not limited to approved tenants.`n") } - $ResultMarkdown += "`n`n## [Collaboration restrictions](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/CompanyRelationshipsMenuBlade/~/Settings/menuId/)`n`n" - $ResultMarkdown += 'The tenant is configured to: ' + $null = $ResultMarkdown.Append("`n`n## [Collaboration restrictions](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/CompanyRelationshipsMenuBlade/~/Settings/menuId/)`n`n") + $null = $ResultMarkdown.Append('The tenant is configured to: ') if ($Passed -eq 'Passed') { - $ResultMarkdown += "**Allow invitations only to the specified domains (most restrictive)** ✅`n" + $null = $ResultMarkdown.Append("**Allow invitations only to the specified domains (most restrictive)** ✅`n") } else { if ($BlockedDomains -and $BlockedDomains.Count -gt 0) { - $ResultMarkdown += "**Deny invitations to the specified domains** ❌`n" + $null = $ResultMarkdown.Append("**Deny invitations to the specified domains** ❌`n") } else { - $ResultMarkdown += "**Allow invitations to be sent to any domain (most inclusive)** ❌`n" + $null = $ResultMarkdown.Append("**Allow invitations to be sent to any domain (most inclusive)** ❌`n") } } if (($AllowedDomains -and $AllowedDomains.Count -gt 0) -or ($BlockedDomains -and $BlockedDomains.Count -gt 0)) { - $ResultMarkdown += "| Domain | Status |`n" - $ResultMarkdown += "| :--- | :--- |`n" + $null = $ResultMarkdown.Append("| Domain | Status |`n") + $null = $ResultMarkdown.Append("| :--- | :--- |`n") foreach ($Domain in $AllowedDomains) { - $ResultMarkdown += "| $Domain | ✅ Allowed |`n" + $null = $ResultMarkdown.Append("| $Domain | ✅ Allowed |`n") } foreach ($Domain in $BlockedDomains) { - $ResultMarkdown += "| $Domain | ❌ Blocked |`n" + $null = $ResultMarkdown.Append("| $Domain | ❌ Blocked |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21824.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21824.ps1 index c2c731d70cb6..6e88785cf1f7 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21824.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21824.ps1 @@ -41,9 +41,9 @@ function Invoke-CippTestZTNA21824 { $reportTitle = 'Sign-in frequency policies' if ($filteredCAPolicies -and $filteredCAPolicies.Count -gt 0) { - $mdInfo = "`n## $reportTitle`n`n" - $mdInfo += "| Policy Name | Sign-in Frequency | Status |`n" - $mdInfo += "| :---------- | :---------------- | :----- |`n" + $mdInfo = [System.Text.StringBuilder]::new("`n## $reportTitle`n`n") + $null = $mdInfo.Append("| Policy Name | Sign-in Frequency | Status |`n") + $null = $mdInfo.Append("| :---------- | :---------------- | :----- |`n") foreach ($filteredCAPolicy in $filteredCAPolicies) { $policyName = $filteredCAPolicy.DisplayName @@ -71,7 +71,7 @@ function Invoke-CippTestZTNA21824 { '❌' } - $mdInfo += "| $policyName | $signInFreqValue | $status |`n" + $null = $mdInfo.Append("| $policyName | $signInFreqValue | $status |`n") } $testResultMarkdown = $testResultMarkdown + $mdInfo diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21825.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21825.ps1 index 635ce60eca4c..be0a438023a5 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21825.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21825.ps1 @@ -27,16 +27,16 @@ function Invoke-CippTestZTNA21825 { # Recommended: Sign-in frequency should be 4 hours or less for privileged users $RecommendedMaxHours = 4 - $ResultMarkdown = "## Privileged User Sign-In Sessions`n`n" - $ResultMarkdown += "**Total Privileged Roles Found:** $($PrivilegedRoles.Count)`n`n" - $ResultMarkdown += "**CA Policies Targeting Roles:** $($RoleScopedPolicies.Count)`n`n" - $ResultMarkdown += "**Recommended Sign In Session Hours:** $RecommendedMaxHours`n`n" - $ResultMarkdown += "### Conditional Access Policies by Privileged Role`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("## Privileged User Sign-In Sessions`n`n") + $null = $ResultMarkdown.Append("**Total Privileged Roles Found:** $($PrivilegedRoles.Count)`n`n") + $null = $ResultMarkdown.Append("**CA Policies Targeting Roles:** $($RoleScopedPolicies.Count)`n`n") + $null = $ResultMarkdown.Append("**Recommended Sign In Session Hours:** $RecommendedMaxHours`n`n") + $null = $ResultMarkdown.Append("### Conditional Access Policies by Privileged Role`n`n") $AllRolesCovered = $true foreach ($Role in $PrivilegedRoles) { - $ResultMarkdown += "#### $($Role.displayName)`n`n" + $null = $ResultMarkdown.Append("#### $($Role.displayName)`n`n") # Get CA policies assigned to this role $AssignedPolicies = $CAPolicies | Where-Object { $_.conditions.users.includeRoles -contains $Role.id } @@ -55,10 +55,10 @@ function Invoke-CippTestZTNA21825 { } else { '❌ Not Covered'; $AllRolesCovered = $false } - $ResultMarkdown += "**Status:** $RoleStatus`n`n" + $null = $ResultMarkdown.Append("**Status:** $RoleStatus`n`n") - $ResultMarkdown += "| Policy Name | Sign-In Frequency | Compliant |`n" - $ResultMarkdown += "| :--- | :--- | :--- |`n" + $null = $ResultMarkdown.Append("| Policy Name | Sign-In Frequency | Compliant |`n") + $null = $ResultMarkdown.Append("| :--- | :--- | :--- |`n") foreach ($Policy in $EnabledPolicies) { $FreqValue = 'Not Configured' @@ -78,12 +78,12 @@ function Invoke-CippTestZTNA21825 { } $PolicyLink = "https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/PolicyBlade/policyId/$($Policy.id)" - $ResultMarkdown += "| [$($Policy.displayName)]($PolicyLink) | $FreqValue | $IsCompliant |`n" + $null = $ResultMarkdown.Append("| [$($Policy.displayName)]($PolicyLink) | $FreqValue | $IsCompliant |`n") } - $ResultMarkdown += "`n" + $null = $ResultMarkdown.Append("`n") } else { - $ResultMarkdown += "**Status:** ❌ No CA policies assigned`n`n" - $ResultMarkdown += "*No Conditional Access policies target this privileged role.*`n`n" + $null = $ResultMarkdown.Append("**Status:** ❌ No CA policies assigned`n`n") + $null = $ResultMarkdown.Append("*No Conditional Access policies target this privileged role.*`n`n") $AllRolesCovered = $false } } @@ -91,10 +91,10 @@ function Invoke-CippTestZTNA21825 { $Passed = if ($AllRolesCovered -and $PrivilegedRoles.Count -gt 0) { 'Passed' } else { 'Failed' } if ($Passed -eq 'Passed') { - $ResultMarkdown += "✅ **All privileged roles are covered by enabled policies enforcing short-lived sessions (≤$RecommendedMaxHours hours).**`n" + $null = $ResultMarkdown.Append("✅ **All privileged roles are covered by enabled policies enforcing short-lived sessions (≤$RecommendedMaxHours hours).**`n") } else { - $ResultMarkdown += "❌ **Not all privileged roles are covered by compliant sign-in frequency controls.**`n" - $ResultMarkdown += "`n**Recommendation:** Configure Conditional Access policies to enforce sign-in frequency of $RecommendedMaxHours hours or less for ALL privileged roles.`n" + $null = $ResultMarkdown.Append("❌ **Not all privileged roles are covered by compliant sign-in frequency controls.**`n") + $null = $ResultMarkdown.Append("`n**Recommendation:** Configure Conditional Access policies to enforce sign-in frequency of $RecommendedMaxHours hours or less for ALL privileged roles.`n") } Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Passed -ResultMarkdown $ResultMarkdown -Risk 'Medium' -Name 'Privileged users have short-lived sign-in sessions' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Access control' diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21828.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21828.ps1 index 78432f754cf2..c29e1c8712a7 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21828.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21828.ps1 @@ -32,14 +32,14 @@ function Invoke-CippTestZTNA21828 { $reportTitle = 'Conditional Access Policies targeting Authentication Transfer' if ($matchedPolicies.Count -gt 0) { - $mdInfo = "`n## $reportTitle`n`n" - $mdInfo += "| Policy Name | Policy ID | State | Created | Modified |`n" - $mdInfo += "| :---------- | :-------- | :---- | :------ | :------- |`n" + $mdInfo = [System.Text.StringBuilder]::new("`n## $reportTitle`n`n") + $null = $mdInfo.Append("| Policy Name | Policy ID | State | Created | Modified |`n") + $null = $mdInfo.Append("| :---------- | :-------- | :---- | :------ | :------- |`n") foreach ($policy in $matchedPolicies) { $created = if ($policy.createdDateTime) { $policy.createdDateTime } else { 'N/A' } $modified = if ($policy.modifiedDateTime) { $policy.modifiedDateTime } else { 'N/A' } - $mdInfo += "| $($policy.displayName) | $($policy.id) | $($policy.state) | $created | $modified |`n" + $null = $mdInfo.Append("| $($policy.displayName) | $($policy.id) | $($policy.state) | $created | $modified |`n") } $testResultMarkdown = $testResultMarkdown + $mdInfo diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21829.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21829.ps1 index 134741579838..661089db8794 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21829.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21829.ps1 @@ -20,15 +20,15 @@ function Invoke-CippTestZTNA21829 { $Passed = if ($FederatedDomains.Count -eq 0) { 'Passed' } else { 'Failed' } if ($Passed -eq 'Passed') { - $ResultMarkdown = "All domains are using cloud authentication.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("All domains are using cloud authentication.`n`n") } else { - $ResultMarkdown = "Federated authentication is in use.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("Federated authentication is in use.`n`n") - $ResultMarkdown += "`n## List of federated domains`n`n" - $ResultMarkdown += "| Domain Name |`n" - $ResultMarkdown += "| :--- |`n" + $null = $ResultMarkdown.Append("`n## List of federated domains`n`n") + $null = $ResultMarkdown.Append("| Domain Name |`n") + $null = $ResultMarkdown.Append("| :--- |`n") foreach ($Domain in $FederatedDomains) { - $ResultMarkdown += "| $($Domain.id) |`n" + $null = $ResultMarkdown.Append("| $($Domain.id) |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21830.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21830.ps1 index a37e509cc499..d35d77867c04 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21830.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21830.ps1 @@ -56,24 +56,24 @@ function Invoke-CippTestZTNA21830 { $Passed = if ($CompliantDevicePolicies.Count -eq 0 -or $DeviceFilterPolicies.Count -eq 0) { 'Failed' } else { 'Passed' } if ($Passed -eq 'Passed') { - $ResultMarkdown = 'Conditional Access policies restrict privileged role access to PAW devices.' + $ResultMarkdown = [System.Text.StringBuilder]::new('Conditional Access policies restrict privileged role access to PAW devices.') } else { - $ResultMarkdown = 'No Conditional Access policies found that restrict privileged roles to PAW device.' + $ResultMarkdown = [System.Text.StringBuilder]::new('No Conditional Access policies found that restrict privileged roles to PAW device.') } $CompliantDeviceMarkdown = if ($CompliantDevicePolicies.Count -gt 0) { '✅' } else { '❌' } $DeviceFilterMarkdown = if ($DeviceFilterPolicies.Count -gt 0) { '✅' } else { '❌' } - $ResultMarkdown += "`n`n**$CompliantDeviceMarkdown Found $($CompliantDevicePolicies.Count) policy(s) with compliant device control targeting all privileged roles**`n" + $null = $ResultMarkdown.Append("`n`n**$CompliantDeviceMarkdown Found $($CompliantDevicePolicies.Count) policy(s) with compliant device control targeting all privileged roles**`n") foreach ($Policy in $CompliantDevicePolicies) { $PortalLink = "https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/PolicyBlade/policyId/$($Policy.id)" - $ResultMarkdown += "- **Policy:** [$($Policy.displayName)]($PortalLink)`n" + $null = $ResultMarkdown.Append("- **Policy:** [$($Policy.displayName)]($PortalLink)`n") } - $ResultMarkdown += "`n`n**$DeviceFilterMarkdown Found $($DeviceFilterPolicies.Count) policy(s) with PAW/SAW device filter targeting all privileged roles**`n" + $null = $ResultMarkdown.Append("`n`n**$DeviceFilterMarkdown Found $($DeviceFilterPolicies.Count) policy(s) with PAW/SAW device filter targeting all privileged roles**`n") foreach ($Policy in $DeviceFilterPolicies) { $PortalLink = "https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/PolicyBlade/policyId/$($Policy.id)" - $ResultMarkdown += "- **Policy:** [$($Policy.displayName)]($PortalLink)`n" + $null = $ResultMarkdown.Append("- **Policy:** [$($Policy.displayName)]($PortalLink)`n") } Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Passed -ResultMarkdown $ResultMarkdown -Risk 'High' -Name 'Conditional Access policies for Privileged Access Workstations are configured' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Application management' diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21835.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21835.ps1 index d30efa2e7327..0fa5d363f333 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21835.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21835.ps1 @@ -124,27 +124,27 @@ function Invoke-CippTestZTNA21835 { $AccountCount = $EmergencyAccessAccounts.Count $Passed = 'Failed' - $ResultMarkdown = '' + $ResultMarkdown = [System.Text.StringBuilder]::new() if ($AccountCount -lt 2) { - $ResultMarkdown = "Fewer than two emergency access accounts were identified based on cloud-only state, registered phishing-resistant credentials and Conditional Access policy exclusions.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("Fewer than two emergency access accounts were identified based on cloud-only state, registered phishing-resistant credentials and Conditional Access policy exclusions.`n`n") } elseif ($AccountCount -ge 2 -and $AccountCount -le 4) { $Passed = 'Passed' - $ResultMarkdown = "Emergency access accounts appear to be configured as per Microsoft guidance based on cloud-only state, registered phishing-resistant credentials and Conditional Access policy exclusions.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("Emergency access accounts appear to be configured as per Microsoft guidance based on cloud-only state, registered phishing-resistant credentials and Conditional Access policy exclusions.`n`n") } else { - $ResultMarkdown = "$AccountCount emergency access accounts appear to be configured based on cloud-only state, registered phishing-resistant credentials and Conditional Access policy exclusions. Review these accounts to determine whether this volume is excessive for your organization.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("$AccountCount emergency access accounts appear to be configured based on cloud-only state, registered phishing-resistant credentials and Conditional Access policy exclusions. Review these accounts to determine whether this volume is excessive for your organization.`n`n") } - $ResultMarkdown += "**Summary:**`n" - $ResultMarkdown += "- Total permanent Global Administrators: $($PermanentGAMembers.Count)`n" - $ResultMarkdown += "- Cloud-only GAs with phishing-resistant auth: $($EmergencyAccountCandidates.Count)`n" - $ResultMarkdown += "- Emergency access accounts (excluded from all CA): $AccountCount`n" - $ResultMarkdown += "- Enabled Conditional Access policies: $($EnabledCAPolicies.Count)`n`n" + $null = $ResultMarkdown.Append("**Summary:**`n") + $null = $ResultMarkdown.Append("- Total permanent Global Administrators: $($PermanentGAMembers.Count)`n") + $null = $ResultMarkdown.Append("- Cloud-only GAs with phishing-resistant auth: $($EmergencyAccountCandidates.Count)`n") + $null = $ResultMarkdown.Append("- Emergency access accounts (excluded from all CA): $AccountCount`n") + $null = $ResultMarkdown.Append("- Enabled Conditional Access policies: $($EnabledCAPolicies.Count)`n`n") if ($EmergencyAccessAccounts.Count -gt 0) { - $ResultMarkdown += "## Emergency access accounts`n`n" - $ResultMarkdown += "| Display name | UPN | Synced from on-premises | Authentication methods |`n" - $ResultMarkdown += "| :----------- | :-- | :---------------------- | :--------------------- |`n" + $null = $ResultMarkdown.Append("## Emergency access accounts`n`n") + $null = $ResultMarkdown.Append("| Display name | UPN | Synced from on-premises | Authentication methods |`n") + $null = $ResultMarkdown.Append("| :----------- | :-- | :---------------------- | :--------------------- |`n") foreach ($Account in $EmergencyAccessAccounts) { $SyncStatus = if ($Account.OnPremisesSyncEnabled -ne $true) { 'No' } else { 'Yes' } @@ -153,15 +153,15 @@ function Invoke-CippTestZTNA21835 { }) -join ', ' $PortalLink = "https://entra.microsoft.com/#view/Microsoft_AAD_UsersAndTenants/UserProfileMenuBlade/~/overview/userId/$($Account.Id)" - $ResultMarkdown += "| $($Account.DisplayName) | [$($Account.UserPrincipalName)]($PortalLink) | $SyncStatus | $AuthMethodDisplay |`n" + $null = $ResultMarkdown.Append("| $($Account.DisplayName) | [$($Account.UserPrincipalName)]($PortalLink) | $SyncStatus | $AuthMethodDisplay |`n") } - $ResultMarkdown += "`n" + $null = $ResultMarkdown.Append("`n") } if ($PermanentGAMembers.Count -gt 0) { - $ResultMarkdown += "## All permanent Global Administrators`n`n" - $ResultMarkdown += "| Display name | UPN | Cloud only | All CA excluded | Phishing resistant auth |`n" - $ResultMarkdown += "| :----------- | :-- | :--------: | :---------: | :---------------------: |`n" + $null = $ResultMarkdown.Append("## All permanent Global Administrators`n`n") + $null = $ResultMarkdown.Append("| Display name | UPN | Cloud only | All CA excluded | Phishing resistant auth |`n") + $null = $ResultMarkdown.Append("| :----------- | :-- | :--------: | :---------: | :---------------------: |`n") $UserSummary = [System.Collections.Generic.List[object]]::new() foreach ($Member in $PermanentGAMembers) { @@ -189,10 +189,10 @@ function Invoke-CippTestZTNA21835 { } foreach ($UserSum in $UserSummary) { - $ResultMarkdown += "| $($UserSum.DisplayName) | [$($UserSum.UserPrincipalName)]($($UserSum.PortalLink)) | $($UserSum.CloudOnly) | $($UserSum.CAExcluded) | $($UserSum.PhishingResistant) |`n" + $null = $ResultMarkdown.Append("| $($UserSum.DisplayName) | [$($UserSum.UserPrincipalName)]($($UserSum.PortalLink)) | $($UserSum.CloudOnly) | $($UserSum.CAExcluded) | $($UserSum.PhishingResistant) |`n") } - $ResultMarkdown += "`n" + $null = $ResultMarkdown.Append("`n") } Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Passed -ResultMarkdown $ResultMarkdown -Risk 'High' -Name 'Emergency access accounts are configured appropriately' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Application management' diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21836.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21836.ps1 index 8253e3737d3d..f7c37115662a 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21836.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21836.ps1 @@ -37,24 +37,24 @@ function Invoke-CippTestZTNA21836 { } $Passed = 'Passed' - $ResultMarkdown = '' + $ResultMarkdown = [System.Text.StringBuilder]::new() if ($WorkloadIdentitiesWithPrivilegedRoles.Count -gt 0) { $Passed = 'Failed' - $ResultMarkdown = "**Found workload identities assigned to privileged roles.**`n" - $ResultMarkdown += "| Service Principal Name | Privileged Role | Assignment Type |`n" - $ResultMarkdown += "| :--- | :--- | :--- |`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("**Found workload identities assigned to privileged roles.**`n") + $null = $ResultMarkdown.Append("| Service Principal Name | Privileged Role | Assignment Type |`n") + $null = $ResultMarkdown.Append("| :--- | :--- | :--- |`n") $SortedAssignments = $WorkloadIdentitiesWithPrivilegedRoles | Sort-Object -Property PrincipalDisplayName foreach ($Assignment in $SortedAssignments) { $SPLink = "https://entra.microsoft.com/#view/Microsoft_AAD_IAM/ManagedAppMenuBlade/~/Overview/objectId/$($Assignment.PrincipalId)/appId/$($Assignment.AppId)/preferredSingleSignOnMode~/null/servicePrincipalType/Application/fromNav/" - $ResultMarkdown += "| [$($Assignment.PrincipalDisplayName)]($SPLink) | $($Assignment.RoleDisplayName) | $($Assignment.AssignmentType) |`n" + $null = $ResultMarkdown.Append("| [$($Assignment.PrincipalDisplayName)]($SPLink) | $($Assignment.RoleDisplayName) | $($Assignment.AssignmentType) |`n") } - $ResultMarkdown += "`n" - $ResultMarkdown += "`n**Recommendation:** Review and remove privileged role assignments from workload identities unless absolutely necessary. Use least-privilege principles and consider alternative approaches like managed identities with specific API permissions instead of directory roles.`n" + $null = $ResultMarkdown.Append("`n") + $null = $ResultMarkdown.Append("`n**Recommendation:** Review and remove privileged role assignments from workload identities unless absolutely necessary. Use least-privilege principles and consider alternative approaches like managed identities with specific API permissions instead of directory roles.`n") } else { - $ResultMarkdown = "✅ **No workload identities found with privileged role assignments.**`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("✅ **No workload identities found with privileged role assignments.**`n") } Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Passed -ResultMarkdown $ResultMarkdown -Risk 'High' -Name 'Workload Identities are not assigned privileged roles' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application management' diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21838.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21838.ps1 index f34f45450fc9..e1088f360404 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21838.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21838.ps1 @@ -28,28 +28,28 @@ function Invoke-CippTestZTNA21838 { $StatusEmoji = if ($Fido2Enabled) { '✅' } else { '❌' } if ($Fido2Enabled) { - $ResultMarkdown = "Security key authentication method is enabled for your tenant, providing hardware-backed phishing-resistant authentication.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("Security key authentication method is enabled for your tenant, providing hardware-backed phishing-resistant authentication.`n`n") } else { - $ResultMarkdown = "Security key authentication method is not enabled; users cannot register FIDO2 security keys for strong authentication.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("Security key authentication method is not enabled; users cannot register FIDO2 security keys for strong authentication.`n`n") } - $ResultMarkdown += "## FIDO2 security key authentication settings`n`n" - $ResultMarkdown += "$StatusEmoji **FIDO2 authentication method**`n" - $ResultMarkdown += "- Status: $($Fido2Config.state)`n" + $null = $ResultMarkdown.Append("## FIDO2 security key authentication settings`n`n") + $null = $ResultMarkdown.Append("$StatusEmoji **FIDO2 authentication method**`n") + $null = $ResultMarkdown.Append("- Status: $($Fido2Config.state)`n") $IncludeTargetsDisplay = if ($Fido2Config.includeTargets -and $Fido2Config.includeTargets.Count -gt 0) { ($Fido2Config.includeTargets | ForEach-Object { if ($_.id -eq 'all_users') { 'All users' } else { $_.id } }) -join ', ' } else { 'None' } - $ResultMarkdown += "- Include targets: $IncludeTargetsDisplay`n" + $null = $ResultMarkdown.Append("- Include targets: $IncludeTargetsDisplay`n") $ExcludeTargetsDisplay = if ($Fido2Config.excludeTargets -and $Fido2Config.excludeTargets.Count -gt 0) { ($Fido2Config.excludeTargets | ForEach-Object { $_.id }) -join ', ' } else { 'None' } - $ResultMarkdown += "- Exclude targets: $ExcludeTargetsDisplay`n" + $null = $ResultMarkdown.Append("- Exclude targets: $ExcludeTargetsDisplay`n") Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Passed -ResultMarkdown $ResultMarkdown -Risk 'High' -Name 'Security key authentication method enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Access control' diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21839.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21839.ps1 index 8d65a5c8dc73..43b19b3b4a6a 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21839.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21839.ps1 @@ -33,35 +33,35 @@ function Invoke-CippTestZTNA21839 { $PortalLink = 'https://entra.microsoft.com/#view/Microsoft_AAD_IAM/AuthenticationMethodsMenuBlade/~/AdminAuthMethods' - $ResultMarkdown = "`n## [Passkey authentication method details]($PortalLink)`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("`n## [Passkey authentication method details]($PortalLink)`n") $StatusDisplay = if ($Fido2Enabled) { 'Enabled ✅' } else { 'Disabled ❌' } - $ResultMarkdown += "- **Status** : $StatusDisplay`n" + $null = $ResultMarkdown.Append("- **Status** : $StatusDisplay`n") if ($Fido2Enabled) { - $ResultMarkdown += '- **Include targets** : ' + $null = $ResultMarkdown.Append('- **Include targets** : ') if ($IncludeTargets) { $TargetsDisplay = ($IncludeTargets | ForEach-Object { if ($_.id -eq 'all_users') { 'All users' } else { $_.id } }) -join ', ' - $ResultMarkdown += "$TargetsDisplay`n" + $null = $ResultMarkdown.Append("$TargetsDisplay`n") } else { - $ResultMarkdown += "None`n" + $null = $ResultMarkdown.Append("None`n") } - $ResultMarkdown += "- **Enforce attestation** : $IsAttestationEnforced`n" + $null = $ResultMarkdown.Append("- **Enforce attestation** : $IsAttestationEnforced`n") if ($KeyRestrictions) { - $ResultMarkdown += "- **Key restriction policy** :`n" + $null = $ResultMarkdown.Append("- **Key restriction policy** :`n") if ($null -ne $KeyRestrictions.isEnforced) { - $ResultMarkdown += " - **Enforce key restrictions** : $($KeyRestrictions.isEnforced)`n" + $null = $ResultMarkdown.Append(" - **Enforce key restrictions** : $($KeyRestrictions.isEnforced)`n") } else { - $ResultMarkdown += " - **Enforce key restrictions** : Not configured`n" + $null = $ResultMarkdown.Append(" - **Enforce key restrictions** : Not configured`n") } if ($KeyRestrictions.enforcementType) { - $ResultMarkdown += " - **Restrict specific keys** : $($KeyRestrictions.enforcementType)`n" + $null = $ResultMarkdown.Append(" - **Restrict specific keys** : $($KeyRestrictions.enforcementType)`n") } else { - $ResultMarkdown += " - **Restrict specific keys** : Not configured`n" + $null = $ResultMarkdown.Append(" - **Restrict specific keys** : Not configured`n") } } } @@ -69,9 +69,9 @@ function Invoke-CippTestZTNA21839 { $Passed = if ($Fido2Enabled -and $HasIncludeTargets) { 'Passed' } else { 'Failed' } if ($Passed -eq 'Passed') { - $ResultMarkdown = "Passkey authentication method is enabled and configured for users in your tenant.$ResultMarkdown" + $ResultMarkdown = [System.Text.StringBuilder]::new("Passkey authentication method is enabled and configured for users in your tenant.$ResultMarkdown") } else { - $ResultMarkdown = "Passkey authentication method is not enabled or not configured for any users in your tenant.$ResultMarkdown" + $ResultMarkdown = [System.Text.StringBuilder]::new("Passkey authentication method is not enabled or not configured for any users in your tenant.$ResultMarkdown") } Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Passed -ResultMarkdown $ResultMarkdown -Risk 'High' -Name 'Passkey authentication method enabled' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Credential management' diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21840.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21840.ps1 index 70c7f74f8ca7..d7e5de935eef 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21840.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21840.ps1 @@ -28,28 +28,28 @@ function Invoke-CippTestZTNA21840 { $PortalLink = 'https://entra.microsoft.com/#view/Microsoft_AAD_IAM/AuthenticationMethodsMenuBlade/~/AdminAuthMethods' - $ResultMarkdown = "`n## [Security key attestation policy details]($PortalLink)`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("`n## [Security key attestation policy details]($PortalLink)`n") $AttestationStatus = if ($IsAttestationEnforced -eq $true) { 'True ✅' } else { 'False ❌' } - $ResultMarkdown += "- **Enforce attestation** : $AttestationStatus`n" + $null = $ResultMarkdown.Append("- **Enforce attestation** : $AttestationStatus`n") if ($KeyRestrictions) { - $ResultMarkdown += "- **Key restriction policy** :`n" + $null = $ResultMarkdown.Append("- **Key restriction policy** :`n") if ($null -ne $KeyRestrictions.isEnforced) { - $ResultMarkdown += " - **Enforce key restrictions** : $($KeyRestrictions.isEnforced)`n" + $null = $ResultMarkdown.Append(" - **Enforce key restrictions** : $($KeyRestrictions.isEnforced)`n") } else { - $ResultMarkdown += " - **Enforce key restrictions** : Not configured`n" + $null = $ResultMarkdown.Append(" - **Enforce key restrictions** : Not configured`n") } if ($KeyRestrictions.enforcementType) { - $ResultMarkdown += " - **Restrict specific keys** : $($KeyRestrictions.enforcementType)`n" + $null = $ResultMarkdown.Append(" - **Restrict specific keys** : $($KeyRestrictions.enforcementType)`n") } else { - $ResultMarkdown += " - **Restrict specific keys** : Not configured`n" + $null = $ResultMarkdown.Append(" - **Restrict specific keys** : Not configured`n") } if ($KeyRestrictions.aaGuids -and $KeyRestrictions.aaGuids.Count -gt 0) { - $ResultMarkdown += " - **AAGUID** :`n" + $null = $ResultMarkdown.Append(" - **AAGUID** :`n") foreach ($Guid in $KeyRestrictions.aaGuids) { - $ResultMarkdown += " - $Guid`n" + $null = $ResultMarkdown.Append(" - $Guid`n") } } } @@ -57,9 +57,9 @@ function Invoke-CippTestZTNA21840 { $Passed = if ($IsAttestationEnforced -eq $true) { 'Passed' } else { 'Failed' } if ($Passed -eq 'Passed') { - $ResultMarkdown = "Security key attestation is properly enforced, ensuring only verified hardware authenticators can be registered.$ResultMarkdown" + $ResultMarkdown = [System.Text.StringBuilder]::new("Security key attestation is properly enforced, ensuring only verified hardware authenticators can be registered.$ResultMarkdown") } else { - $ResultMarkdown = "Security key attestation is not enforced, allowing unverified or potentially compromised security keys to be registered.$ResultMarkdown" + $ResultMarkdown = [System.Text.StringBuilder]::new("Security key attestation is not enforced, allowing unverified or potentially compromised security keys to be registered.$ResultMarkdown") } Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Passed -ResultMarkdown $ResultMarkdown -Risk 'High' -Name 'Security key attestation is enforced' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21845.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21845.ps1 index e6d305282de3..9021fa574bf6 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21845.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21845.ps1 @@ -43,25 +43,25 @@ function Invoke-CippTestZTNA21845 { $Passed = 'Failed' if ($TAPEnabled -and $TargetsAllUsers -and $HasConditionalAccessEnforcement -and $TAPSupportedInAuthStrength) { $Passed = 'Passed' - $ResultMarkdown = 'Temporary Access Pass is enabled, targeting all users, and enforced with conditional access policies.' + $ResultMarkdown = [System.Text.StringBuilder]::new('Temporary Access Pass is enabled, targeting all users, and enforced with conditional access policies.') } elseif ($TAPEnabled -and $TargetsAllUsers -and $HasConditionalAccessEnforcement -and -not $TAPSupportedInAuthStrength) { - $ResultMarkdown = "Temporary Access Pass is enabled but authentication strength policies don't include TAP methods." + $ResultMarkdown = [System.Text.StringBuilder]::new("Temporary Access Pass is enabled but authentication strength policies don't include TAP methods.") } elseif ($TAPEnabled -and $TargetsAllUsers -and -not $HasConditionalAccessEnforcement) { - $ResultMarkdown = 'Temporary Access Pass is enabled but no conditional access enforcement for security info registration found. Consider adding conditional access policies for stronger security.' + $ResultMarkdown = [System.Text.StringBuilder]::new('Temporary Access Pass is enabled but no conditional access enforcement for security info registration found. Consider adding conditional access policies for stronger security.') } else { - $ResultMarkdown = 'Temporary Access Pass is not properly configured or does not target all users.' + $ResultMarkdown = [System.Text.StringBuilder]::new('Temporary Access Pass is not properly configured or does not target all users.') } - $ResultMarkdown += "`n`n**Configuration summary**`n`n" + $null = $ResultMarkdown.Append("`n`n**Configuration summary**`n`n") $TAPStatus = if ($TAPConfig.state -eq 'enabled') { 'Enabled ✅' } else { 'Disabled ❌' } - $ResultMarkdown += "[Temporary Access Pass](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/AuthenticationMethodsMenuBlade/~/AdminAuthMethods/fromNav/Identity): $TAPStatus`n`n" + $null = $ResultMarkdown.Append("[Temporary Access Pass](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/AuthenticationMethodsMenuBlade/~/AdminAuthMethods/fromNav/Identity): $TAPStatus`n`n") $CAStatus = if ($HasConditionalAccessEnforcement) { 'Enabled ✅' } else { 'Not enabled ❌' } - $ResultMarkdown += "[Conditional Access policy for Security info registration](https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/ConditionalAccessBlade/~/Policies/fromNav/Identity): $CAStatus`n`n" + $null = $ResultMarkdown.Append("[Conditional Access policy for Security info registration](https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/ConditionalAccessBlade/~/Policies/fromNav/Identity): $CAStatus`n`n") $AuthStrengthStatus = if ($TAPSupportedInAuthStrength) { 'Enabled ✅' } else { 'Not enabled ❌' } - $ResultMarkdown += "[Authentication strength policy for Temporary Access Pass](https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/AuthenticationStrength.ReactView/fromNav/Identity): $AuthStrengthStatus`n" + $null = $ResultMarkdown.Append("[Authentication strength policy for Temporary Access Pass](https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/AuthenticationStrength.ReactView/fromNav/Identity): $AuthStrengthStatus`n") Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Passed -ResultMarkdown $ResultMarkdown -Risk 'Medium' -Name 'Temporary access pass is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21846.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21846.ps1 index 6db468ee206c..241787f5ec71 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21846.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21846.ps1 @@ -20,19 +20,19 @@ function Invoke-CippTestZTNA21846 { $Passed = if ($TAPConfig.isUsableOnce -eq $true) { 'Passed' } else { 'Failed' } if ($Passed -eq 'Passed') { - $ResultMarkdown = "Temporary Access Pass is configured for one-time use only.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("Temporary Access Pass is configured for one-time use only.`n`n") } else { - $ResultMarkdown = "Temporary Access Pass allows multiple uses during validity period.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("Temporary Access Pass allows multiple uses during validity period.`n`n") } - $ResultMarkdown += "## Temporary Access Pass Configuration`n`n" - $ResultMarkdown += "| Setting | Value | Status |`n" - $ResultMarkdown += "| :------ | :---- | :----- |`n" + $null = $ResultMarkdown.Append("## Temporary Access Pass Configuration`n`n") + $null = $ResultMarkdown.Append("| Setting | Value | Status |`n") + $null = $ResultMarkdown.Append("| :------ | :---- | :----- |`n") $IsUsableOnceValue = if ($TAPConfig.isUsableOnce) { 'Enabled' } else { 'Disabled' } $StatusEmoji = if ($Passed -eq 'Passed') { '✅ Pass' } else { '❌ Fail' } - $ResultMarkdown += "| [One-time use restriction](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/AuthenticationMethodsMenuBlade/~/AdminAuthMethods/fromNav/) | $IsUsableOnceValue | $StatusEmoji |`n" + $null = $ResultMarkdown.Append("| [One-time use restriction](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/AuthenticationMethodsMenuBlade/~/AdminAuthMethods/fromNav/) | $IsUsableOnceValue | $StatusEmoji |`n") Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Passed -ResultMarkdown $ResultMarkdown -Risk 'Medium' -Name 'Restrict Temporary Access Pass to Single Use' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21848.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21848.ps1 index 92957ca70c24..a68c36ebcef2 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21848.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21848.ps1 @@ -46,15 +46,15 @@ function Invoke-CippTestZTNA21848 { } if ($Passed -eq 'Passed') { - $ResultMarkdown = "✅ Custom banned passwords are properly configured with organization-specific terms.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("✅ Custom banned passwords are properly configured with organization-specific terms.`n`n") } else { - $ResultMarkdown = "❌ Custom banned passwords are not enabled or lack organization-specific terms.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("❌ Custom banned passwords are not enabled or lack organization-specific terms.`n`n") } - $ResultMarkdown += "## [Password protection settings]($PortalLink)`n`n" - $ResultMarkdown += "| Enforce custom list | Custom banned password list | Number of terms |`n" - $ResultMarkdown += "| :------------------ | :-------------------------- | :-------------- |`n" - $ResultMarkdown += "| $Enforced | $($DisplayList -join ', ') | $($BannedPasswordArray.Count) |`n" + $null = $ResultMarkdown.Append("## [Password protection settings]($PortalLink)`n`n") + $null = $ResultMarkdown.Append("| Enforce custom list | Custom banned password list | Number of terms |`n") + $null = $ResultMarkdown.Append("| :------------------ | :-------------------------- | :-------------- |`n") + $null = $ResultMarkdown.Append("| $Enforced | $($DisplayList -join ', ') | $($BannedPasswordArray.Count) |`n") Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Passed -ResultMarkdown $ResultMarkdown -Risk 'Medium' -Name 'Add organizational terms to the banned password list' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21849.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21849.ps1 index fc2a359a35d6..db14d7067bd1 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21849.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21849.ps1 @@ -17,37 +17,37 @@ function Invoke-CippTestZTNA21849 { if ($null -eq $PasswordRuleSettings) { # Default is 60 seconds $Passed = 'Passed' - $ResultMarkdown = "✅ Smart Lockout duration is configured to 60 seconds or higher (default).`n`n" - $ResultMarkdown += "## [Smart Lockout Settings]($PortalLink)`n`n" - $ResultMarkdown += "| Setting | Value |`n" - $ResultMarkdown += "| :---- | :---- |`n" - $ResultMarkdown += "| Lockout Duration (seconds) | 60 (Default) |`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("✅ Smart Lockout duration is configured to 60 seconds or higher (default).`n`n") + $null = $ResultMarkdown.Append("## [Smart Lockout Settings]($PortalLink)`n`n") + $null = $ResultMarkdown.Append("| Setting | Value |`n") + $null = $ResultMarkdown.Append("| :---- | :---- |`n") + $null = $ResultMarkdown.Append("| Lockout Duration (seconds) | 60 (Default) |`n") } else { $LockoutDurationSetting = $PasswordRuleSettings.values | Where-Object { $_.name -eq 'LockoutDurationInSeconds' } if ($null -eq $LockoutDurationSetting) { # Default is 60 seconds $Passed = 'Passed' - $ResultMarkdown = "✅ Smart Lockout duration is configured to 60 seconds or higher (default).`n`n" - $ResultMarkdown += "## [Smart Lockout Settings]($PortalLink)`n`n" - $ResultMarkdown += "| Setting | Value |`n" - $ResultMarkdown += "| :---- | :---- |`n" - $ResultMarkdown += "| Lockout Duration (seconds) | 60 (Default) |`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("✅ Smart Lockout duration is configured to 60 seconds or higher (default).`n`n") + $null = $ResultMarkdown.Append("## [Smart Lockout Settings]($PortalLink)`n`n") + $null = $ResultMarkdown.Append("| Setting | Value |`n") + $null = $ResultMarkdown.Append("| :---- | :---- |`n") + $null = $ResultMarkdown.Append("| Lockout Duration (seconds) | 60 (Default) |`n") } else { $LockoutDuration = [int]$LockoutDurationSetting.value if ($LockoutDuration -ge 60) { $Passed = 'Passed' - $ResultMarkdown = "✅ Smart Lockout duration is configured to 60 seconds or higher.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("✅ Smart Lockout duration is configured to 60 seconds or higher.`n`n") } else { $Passed = 'Failed' - $ResultMarkdown = "❌ Smart Lockout duration is configured below 60 seconds.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("❌ Smart Lockout duration is configured below 60 seconds.`n`n") } - $ResultMarkdown += "## [Smart Lockout Settings]($PortalLink)`n`n" - $ResultMarkdown += "| Setting | Value |`n" - $ResultMarkdown += "| :---- | :---- |`n" - $ResultMarkdown += "| Lockout Duration (seconds) | $LockoutDuration |`n" + $null = $ResultMarkdown.Append("## [Smart Lockout Settings]($PortalLink)`n`n") + $null = $ResultMarkdown.Append("| Setting | Value |`n") + $null = $ResultMarkdown.Append("| :---- | :---- |`n") + $null = $ResultMarkdown.Append("| Lockout Duration (seconds) | $LockoutDuration |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21850.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21850.ps1 index 3e3708422b81..cbd2c7d6dbe0 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21850.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21850.ps1 @@ -16,28 +16,28 @@ function Invoke-CippTestZTNA21850 { if ($null -eq $PasswordRuleSettings) { $Passed = 'Failed' - $ResultMarkdown = '❌ Password rule settings template not found.' + $ResultMarkdown = [System.Text.StringBuilder]::new('❌ Password rule settings template not found.') } else { $LockoutThresholdSetting = $PasswordRuleSettings.values | Where-Object { $_.name -eq 'LockoutThreshold' } if ($null -eq $LockoutThresholdSetting) { $Passed = 'Failed' - $ResultMarkdown = "❌ Lockout threshold setting not found in [password rule settings]($PortalLink)." + $ResultMarkdown = [System.Text.StringBuilder]::new("❌ Lockout threshold setting not found in [password rule settings]($PortalLink).") } else { $LockoutThreshold = [int]$LockoutThresholdSetting.value if ($LockoutThreshold -le 10) { $Passed = 'Passed' - $ResultMarkdown = "✅ Smart lockout threshold is set to 10 or below.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("✅ Smart lockout threshold is set to 10 or below.`n`n") } else { $Passed = 'Failed' - $ResultMarkdown = "❌ Smart lockout threshold is configured above 10.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("❌ Smart lockout threshold is configured above 10.`n`n") } - $ResultMarkdown += "## [Smart lockout configuration]($PortalLink)`n`n" - $ResultMarkdown += "| Setting | Value |`n" - $ResultMarkdown += "| :---- | :---- |`n" - $ResultMarkdown += "| Lockout threshold | $LockoutThreshold attempts |`n" + $null = $ResultMarkdown.Append("## [Smart lockout configuration]($PortalLink)`n`n") + $null = $ResultMarkdown.Append("| Setting | Value |`n") + $null = $ResultMarkdown.Append("| :---- | :---- |`n") + $null = $ResultMarkdown.Append("| Lockout threshold | $LockoutThreshold attempts |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21861.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21861.ps1 index baed2106b48c..b22f2ed98354 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21861.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21861.ps1 @@ -22,12 +22,12 @@ function Invoke-CippTestZTNA21861 { $Passed = if ($UntriagedHighRiskUsers.Count -eq 0) { 'Passed' } else { 'Failed' } if ($Passed -eq 'Passed') { - $ResultMarkdown = '✅ All high-risk users are properly triaged in Entra ID Protection.' + $ResultMarkdown = [System.Text.StringBuilder]::new('✅ All high-risk users are properly triaged in Entra ID Protection.') } else { - $ResultMarkdown = "❌ Found **$($UntriagedHighRiskUsers.Count)** untriaged high-risk users in Entra ID Protection.`n`n" - $ResultMarkdown += "## Untriaged High-Risk Users`n`n" - $ResultMarkdown += "| User | Risk level | Last updated | Risk detail |`n" - $ResultMarkdown += "| :--- | :--- | :--- | :--- |`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("❌ Found **$($UntriagedHighRiskUsers.Count)** untriaged high-risk users in Entra ID Protection.`n`n") + $null = $ResultMarkdown.Append("## Untriaged High-Risk Users`n`n") + $null = $ResultMarkdown.Append("| User | Risk level | Last updated | Risk detail |`n") + $null = $ResultMarkdown.Append("| :--- | :--- | :--- | :--- |`n") foreach ($User in $UntriagedHighRiskUsers) { $UserPrincipalName = if ($User.userPrincipalName) { $User.userPrincipalName } else { $User.id } @@ -41,7 +41,7 @@ function Invoke-CippTestZTNA21861 { $RiskDetail = $User.riskDetail $PortalLink = "https://entra.microsoft.com/#view/Microsoft_AAD_UsersAndTenants/UserProfileMenuBlade/~/overview/userId/$($User.id)" - $ResultMarkdown += "| [$UserPrincipalName]($PortalLink) | $RiskLevel | $RiskDate | $RiskDetail |`n" + $null = $ResultMarkdown.Append("| [$UserPrincipalName]($PortalLink) | $RiskLevel | $RiskDate | $RiskDetail |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21862.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21862.ps1 index c5c00bc795c6..fe5726e31390 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21862.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21862.ps1 @@ -21,16 +21,16 @@ function Invoke-CippTestZTNA21862 { $Passed = if (($UntriagedRiskyPrincipals.Count -eq 0) -and ($UntriagedRiskDetections.Count -eq 0)) { 'Passed' } else { 'Failed' } if ($Passed -eq 'Passed') { - $ResultMarkdown = '✅ All risky workload identities have been triaged' + $ResultMarkdown = [System.Text.StringBuilder]::new('✅ All risky workload identities have been triaged') } else { $RiskySPCount = $UntriagedRiskyPrincipals.Count $RiskyDetectionCount = $UntriagedRiskDetections.Count - $ResultMarkdown = "❌ Found $RiskySPCount untriaged risky service principals and $RiskyDetectionCount untriaged risk detections`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("❌ Found $RiskySPCount untriaged risky service principals and $RiskyDetectionCount untriaged risk detections`n`n") if ($RiskySPCount -gt 0) { - $ResultMarkdown += "## Untriaged Risky Service Principals`n`n" - $ResultMarkdown += "| Service Principal | Type | Risk Level | Risk State | Risk Last Updated |`n" - $ResultMarkdown += "| :--- | :--- | :--- | :--- | :--- |`n" + $null = $ResultMarkdown.Append("## Untriaged Risky Service Principals`n`n") + $null = $ResultMarkdown.Append("| Service Principal | Type | Risk Level | Risk State | Risk Last Updated |`n") + $null = $ResultMarkdown.Append("| :--- | :--- | :--- | :--- | :--- |`n") foreach ($SP in $UntriagedRiskyPrincipals) { $PortalLink = "https://entra.microsoft.com/#view/Microsoft_AAD_IAM/ManagedAppMenuBlade/~/SignOn/objectId/$($SP.id)/appId/$($SP.appId)" $RiskLevel = switch ($SP.riskLevel) { @@ -46,14 +46,14 @@ function Invoke-CippTestZTNA21862 { 'remediated' { '✅ Remediated' } default { $SP.riskState } } - $ResultMarkdown += "| [$($SP.displayName)]($PortalLink) | $($SP.servicePrincipalType) | $RiskLevel | $RiskState | $($SP.riskLastUpdatedDateTime) |`n" + $null = $ResultMarkdown.Append("| [$($SP.displayName)]($PortalLink) | $($SP.servicePrincipalType) | $RiskLevel | $RiskState | $($SP.riskLastUpdatedDateTime) |`n") } } if ($RiskyDetectionCount -gt 0) { - $ResultMarkdown += "`n`n## Untriaged Risk Detection Events`n`n" - $ResultMarkdown += "| Service Principal | Risk Level | Risk State | Risk Event Type | Risk Last Updated |`n" - $ResultMarkdown += "| :--- | :--- | :--- | :--- | :--- |`n" + $null = $ResultMarkdown.Append("`n`n## Untriaged Risk Detection Events`n`n") + $null = $ResultMarkdown.Append("| Service Principal | Risk Level | Risk State | Risk Event Type | Risk Last Updated |`n") + $null = $ResultMarkdown.Append("| :--- | :--- | :--- | :--- | :--- |`n") foreach ($Detection in $UntriagedRiskDetections) { $PortalLink = "https://entra.microsoft.com/#view/Microsoft_AAD_IAM/ManagedAppMenuBlade/~/SignOn/objectId/$($Detection.servicePrincipalId)/appId/$($Detection.appId)" $RiskLevel = switch ($Detection.riskLevel) { @@ -69,7 +69,7 @@ function Invoke-CippTestZTNA21862 { 'remediated' { '✅ Remediated' } default { $Detection.riskState } } - $ResultMarkdown += "| [$($Detection.servicePrincipalDisplayName)]($PortalLink) | $RiskLevel | $RiskState | $($Detection.riskEventType) | $($Detection.detectedDateTime) |`n" + $null = $ResultMarkdown.Append("| [$($Detection.servicePrincipalDisplayName)]($PortalLink) | $RiskLevel | $RiskState | $($Detection.riskEventType) | $($Detection.detectedDateTime) |`n") } } } diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21863.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21863.ps1 index a6d40ba2445e..688ba6438366 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21863.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21863.ps1 @@ -21,12 +21,12 @@ function Invoke-CippTestZTNA21863 { $Passed = if ($UntriagedHighRiskSignIns.Count -eq 0) { 'Passed' } else { 'Failed' } if ($Passed -eq 'Passed') { - $ResultMarkdown = '✅ No untriaged risky sign ins in the tenant.' + $ResultMarkdown = [System.Text.StringBuilder]::new('✅ No untriaged risky sign ins in the tenant.') } else { - $ResultMarkdown = "❌ Found **$($UntriagedHighRiskSignIns.Count)** untriaged high-risk sign ins.`n`n" - $ResultMarkdown += "## Untriaged High-Risk Sign ins`n`n" - $ResultMarkdown += "| Date | User Principal Name | Type | Risk Level |`n" - $ResultMarkdown += "| :---- | :---- | :---- | :---- |`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("❌ Found **$($UntriagedHighRiskSignIns.Count)** untriaged high-risk sign ins.`n`n") + $null = $ResultMarkdown.Append("## Untriaged High-Risk Sign ins`n`n") + $null = $ResultMarkdown.Append("| Date | User Principal Name | Type | Risk Level |`n") + $null = $ResultMarkdown.Append("| :---- | :---- | :---- | :---- |`n") foreach ($Risk in $UntriagedHighRiskSignIns) { $UserPrincipalName = $Risk.userPrincipalName @@ -38,7 +38,7 @@ function Invoke-CippTestZTNA21863 { } $RiskEventType = $Risk.riskEventType $RiskDate = $Risk.detectedDateTime - $ResultMarkdown += "| $RiskDate | $UserPrincipalName | $RiskEventType | $RiskLevel |`n" + $null = $ResultMarkdown.Append("| $RiskDate | $UserPrincipalName | $RiskEventType | $RiskLevel |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21865.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21865.ps1 index 0aa3d5bb3837..3043fc8ec517 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21865.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21865.ps1 @@ -19,17 +19,17 @@ function Invoke-CippTestZTNA21865 { $Passed = $TrustedLocations.Count -gt 0 if ($Passed) { - $ResultMarkdown = "✅ Trusted named locations are configured.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("✅ Trusted named locations are configured.`n`n") } else { - $ResultMarkdown = "❌ No trusted named locations configured.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("❌ No trusted named locations configured.`n`n") } - $ResultMarkdown += "## Named Locations`n`n" - $ResultMarkdown += "$($NamedLocations.Count) named locations found.`n`n" + $null = $ResultMarkdown.Append("## Named Locations`n`n") + $null = $ResultMarkdown.Append("$($NamedLocations.Count) named locations found.`n`n") if ($NamedLocations.Count -gt 0) { - $ResultMarkdown += "| Name | Type | Trusted |`n" - $ResultMarkdown += "| :--- | :--- | :------ |`n" + $null = $ResultMarkdown.Append("| Name | Type | Trusted |`n") + $null = $ResultMarkdown.Append("| :--- | :--- | :------ |`n") foreach ($Location in $NamedLocations) { $Name = $Location.displayName @@ -37,7 +37,7 @@ function Invoke-CippTestZTNA21865 { elseif ($Location.'@odata.type' -eq '#microsoft.graph.countryNamedLocation') { 'Country-based' } else { 'Unknown' } $Trusted = if ($Location.isTrusted) { 'Yes' } else { 'No' } - $ResultMarkdown += "| $Name | $Type | $Trusted |`n" + $null = $ResultMarkdown.Append("| $Name | $Type | $Trusted |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21866.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21866.ps1 index 6bcc48f48d77..fecd3a3fe9b8 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21866.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21866.ps1 @@ -22,19 +22,19 @@ function Invoke-CippTestZTNA21866 { $Passed = if ($UnaddressedRecommendations.Count -eq 0) { 'Passed' } else { 'Failed' } if ($Passed -eq 'Passed') { - $ResultMarkdown = '✅ All Entra Recommendations are addressed.' + $ResultMarkdown = [System.Text.StringBuilder]::new('✅ All Entra Recommendations are addressed.') } else { - $ResultMarkdown = "❌ Found $($UnaddressedRecommendations.Count) unaddressed Entra recommendations.`n`n" - $ResultMarkdown += "## Unaddressed Entra recommendations`n`n" - $ResultMarkdown += "| Display Name | Status | Insights | Priority |`n" - $ResultMarkdown += "| :--- | :--- | :--- | :--- |`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("❌ Found $($UnaddressedRecommendations.Count) unaddressed Entra recommendations.`n`n") + $null = $ResultMarkdown.Append("## Unaddressed Entra recommendations`n`n") + $null = $ResultMarkdown.Append("| Display Name | Status | Insights | Priority |`n") + $null = $ResultMarkdown.Append("| :--- | :--- | :--- | :--- |`n") foreach ($Item in $UnaddressedRecommendations) { $DisplayName = $Item.displayName $Status = $Item.status $Insights = $Item.insights $Priority = $Item.priority - $ResultMarkdown += "| $DisplayName | $Status | $Insights | $Priority |`n" + $null = $ResultMarkdown.Append("| $DisplayName | $Status | $Insights | $Priority |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21872.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21872.ps1 index b2a689e4c7f4..5c547748f391 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21872.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21872.ps1 @@ -54,32 +54,32 @@ function Invoke-CippTestZTNA21872 { # Determine pass/fail conditions if ($MfaRequiredInDeviceSettings) { $Passed = 'Failed' - $ResultMarkdown = "❌ **MFA is configured incorrectly.** Device Settings has 'Require Multi-Factor Authentication to register or join devices' set to Yes. According to best practices, this should be set to No, and MFA should be enforced through Conditional Access policies instead.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("❌ **MFA is configured incorrectly.** Device Settings has 'Require Multi-Factor Authentication to register or join devices' set to Yes. According to best practices, this should be set to No, and MFA should be enforced through Conditional Access policies instead.`n`n") } elseif ($DeviceRegistrationPolicies.Count -eq 0) { $Passed = 'Failed' - $ResultMarkdown = "❌ **No Conditional Access policies found** for device registration or device join. Create a policy that requires MFA for these user actions.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("❌ **No Conditional Access policies found** for device registration or device join. Create a policy that requires MFA for these user actions.`n`n") } elseif ($ValidPolicies.Count -eq 0) { $Passed = 'Failed' - $ResultMarkdown = "❌ **Conditional Access policies found**, but they're not correctly configured. Policies should require MFA or appropriate authentication strength.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("❌ **Conditional Access policies found**, but they're not correctly configured. Policies should require MFA or appropriate authentication strength.`n`n") } else { $Passed = 'Passed' - $ResultMarkdown = "✅ **Properly configured Conditional Access policies found** that require MFA for device registration/join actions.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("✅ **Properly configured Conditional Access policies found** that require MFA for device registration/join actions.`n`n") } # Add device settings information - $ResultMarkdown += "## Device Settings Configuration`n`n" - $ResultMarkdown += "| Setting | Value | Recommended Value | Status |`n" - $ResultMarkdown += "| :------ | :---- | :---------------- | :----- |`n" + $null = $ResultMarkdown.Append("## Device Settings Configuration`n`n") + $null = $ResultMarkdown.Append("| Setting | Value | Recommended Value | Status |`n") + $null = $ResultMarkdown.Append("| :------ | :---- | :---------------- | :----- |`n") $DeviceSettingStatus = if ($MfaRequiredInDeviceSettings) { '❌ Should be set to No' } else { '✅ Correctly configured' } $DeviceSettingValue = if ($MfaRequiredInDeviceSettings) { 'Yes' } else { 'No' } - $ResultMarkdown += "| Require Multi-Factor Authentication to register or join devices | $DeviceSettingValue | No | $DeviceSettingStatus |`n" + $null = $ResultMarkdown.Append("| Require Multi-Factor Authentication to register or join devices | $DeviceSettingValue | No | $DeviceSettingStatus |`n") # Add policies information if any found if ($DeviceRegistrationPolicies.Count -gt 0) { - $ResultMarkdown += "`n## Device Registration/Join Conditional Access Policies`n`n" - $ResultMarkdown += "| Policy Name | State | Requires MFA | Status |`n" - $ResultMarkdown += "| :---------- | :---- | :----------- | :----- |`n" + $null = $ResultMarkdown.Append("`n## Device Registration/Join Conditional Access Policies`n`n") + $null = $ResultMarkdown.Append("| Policy Name | State | Requires MFA | Status |`n") + $null = $ResultMarkdown.Append("| :---------- | :---- | :----------- | :----- |`n") foreach ($Policy in $DeviceRegistrationPolicies) { $PolicyName = $Policy.displayName @@ -92,7 +92,7 @@ function Invoke-CippTestZTNA21872 { $RequiresMfaText = if ($IsValid) { 'Yes' } else { 'No' } $StatusText = if ($IsValid) { '✅ Properly configured' } else { '❌ Incorrectly configured' } - $ResultMarkdown += "| $PolicyNameLink | $PolicyState | $RequiresMfaText | $StatusText |`n" + $null = $ResultMarkdown.Append("| $PolicyNameLink | $PolicyState | $RequiresMfaText | $StatusText |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21883.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21883.ps1 index b99980e3f165..1973fe5e4c0a 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21883.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21883.ps1 @@ -70,10 +70,10 @@ function Invoke-CippTestZTNA21883 { # Determine pass/fail if ($MatchedPolicies.Count -ge 1) { $Status = 'Passed' - $ResultMarkdown = "✅ **Pass**: Workload identities are protected by risk-based Conditional Access policies.`n`n" - $ResultMarkdown += "## Matching policies`n`n" - $ResultMarkdown += "| Policy name | State | Service principals | Grant controls |`n" - $ResultMarkdown += "| :---------- | :---- | :----------------- | :------------- |`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("✅ **Pass**: Workload identities are protected by risk-based Conditional Access policies.`n`n") + $null = $ResultMarkdown.Append("## Matching policies`n`n") + $null = $ResultMarkdown.Append("| Policy name | State | Service principals | Grant controls |`n") + $null = $ResultMarkdown.Append("| :---------- | :---- | :----------------- | :------------- |`n") foreach ($Policy in $MatchedPolicies) { $policyLink = "https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/PolicyBlade/policyId/$($Policy.id)" @@ -92,12 +92,12 @@ function Invoke-CippTestZTNA21883 { } else { 'None' } - $ResultMarkdown += "| [$policyName]($policyLink) | $($Policy.state) | $spTargets | $grants |`n" + $null = $ResultMarkdown.Append("| [$policyName]($policyLink) | $($Policy.state) | $spTargets | $grants |`n") } } else { $Status = 'Failed' - $ResultMarkdown = "❌ **Fail**: No Conditional Access policies found that protect workload identities with risk-based controls.`n`n" - $ResultMarkdown += 'Workload identities should be protected by policies that block authentication when service principal risk is detected.' + $ResultMarkdown = [System.Text.StringBuilder]::new("❌ **Fail**: No Conditional Access policies found that protect workload identities with risk-based controls.`n`n") + $null = $ResultMarkdown.Append('Workload identities should be protected by policies that block authentication when service principal risk is detected.') } $TestParams = @{ diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21889.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21889.ps1 index f7e88c66552e..b357ea8add45 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21889.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21889.ps1 @@ -83,15 +83,15 @@ function Invoke-CippTestZTNA21889 { # Build result message if ($Status -eq 'Passed') { - $ResultMarkdown = "✅ **Pass**: Your organization has implemented multiple passwordless authentication methods reducing password exposure.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("✅ **Pass**: Your organization has implemented multiple passwordless authentication methods reducing password exposure.`n`n") } else { - $ResultMarkdown = "❌ **Fail**: Your organization relies heavily on password-based authentication, creating security vulnerabilities.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("❌ **Fail**: Your organization relies heavily on password-based authentication, creating security vulnerabilities.`n`n") } # Build detailed markdown table - $ResultMarkdown += "## Passwordless authentication methods`n`n" - $ResultMarkdown += "| Method | State | Include targets | Authentication mode | Status |`n" - $ResultMarkdown += "| :----- | :---- | :-------------- | :------------------ | :----- |`n" + $null = $ResultMarkdown.Append("## Passwordless authentication methods`n`n") + $null = $ResultMarkdown.Append("| Method | State | Include targets | Authentication mode | Status |`n") + $null = $ResultMarkdown.Append("| :----- | :---- | :-------------- | :------------------ | :----- |`n") # FIDO2 row $Fido2State = if ($Fido2Enabled) { '✅ Enabled' } else { '❌ Disabled' } @@ -101,7 +101,7 @@ function Invoke-CippTestZTNA21889 { 'None' } $Fido2Status = if ($Fido2Valid) { '✅ Pass' } else { '❌ Fail' } - $ResultMarkdown += "| FIDO2 Security Keys | $Fido2State | $Fido2TargetsDisplay | N/A | $Fido2Status |`n" + $null = $ResultMarkdown.Append("| FIDO2 Security Keys | $Fido2State | $Fido2TargetsDisplay | N/A | $Fido2Status |`n") # Microsoft Authenticator row $AuthState = if ($AuthEnabled) { '✅ Enabled' } else { '❌ Disabled' } @@ -112,7 +112,7 @@ function Invoke-CippTestZTNA21889 { } $AuthModeDisplay = if ($AuthModeValid) { "✅ $AuthMode" } else { "❌ $AuthMode" } $AuthStatus = if ($AuthValid) { '✅ Pass' } else { '❌ Fail' } - $ResultMarkdown += "| Microsoft Authenticator | $AuthState | $AuthTargetsDisplay | $AuthModeDisplay | $AuthStatus |`n" + $null = $ResultMarkdown.Append("| Microsoft Authenticator | $AuthState | $AuthTargetsDisplay | $AuthModeDisplay | $AuthStatus |`n") $TestParams = @{ TestId = 'ZTNA21889' diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21892.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21892.ps1 index 404834bc99a9..7464fdf309fe 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21892.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21892.ps1 @@ -99,10 +99,10 @@ function Invoke-CippTestZTNA21892 { # Determine pass/fail if ($MatchingPolicies.Count -gt 0) { $Status = 'Passed' - $ResultMarkdown = "✅ **Pass**: Conditional Access policies require managed devices for all sign-in activity.`n`n" - $ResultMarkdown += "## Matching policies`n`n" - $ResultMarkdown += "| Policy name | State | All users | All apps | Compliant device | Hybrid joined |`n" - $ResultMarkdown += "| :---------- | :---- | :-------- | :------- | :--------------- | :------------ |`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("✅ **Pass**: Conditional Access policies require managed devices for all sign-in activity.`n`n") + $null = $ResultMarkdown.Append("## Matching policies`n`n") + $null = $ResultMarkdown.Append("| Policy name | State | All users | All apps | Compliant device | Hybrid joined |`n") + $null = $ResultMarkdown.Append("| :---------- | :---- | :-------- | :------- | :--------------- | :------------ |`n") foreach ($Policy in $MatchingPolicies) { $policyLink = "https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/PolicyBlade/policyId/$($Policy.PolicyId)" @@ -112,12 +112,12 @@ function Invoke-CippTestZTNA21892 { $compliant = if ($Policy.CompliantDevice) { '✅' } else { '❌' } $hybrid = if ($Policy.HybridJoinedDevice) { '✅' } else { '❌' } - $ResultMarkdown += "| [$policyName]($policyLink) | $($Policy.PolicyState) | $allUsers | $allApps | $compliant | $hybrid |`n" + $null = $ResultMarkdown.Append("| [$policyName]($policyLink) | $($Policy.PolicyState) | $allUsers | $allApps | $compliant | $hybrid |`n") } } else { $Status = 'Failed' - $ResultMarkdown = "❌ **Fail**: No Conditional Access policies found that require managed devices for all sign-in activity.`n`n" - $ResultMarkdown += 'Organizations should enforce that all sign-ins come from managed devices (compliant or hybrid Azure AD joined) to ensure security controls are applied.' + $ResultMarkdown = [System.Text.StringBuilder]::new("❌ **Fail**: No Conditional Access policies found that require managed devices for all sign-in activity.`n`n") + $null = $ResultMarkdown.Append('Organizations should enforce that all sign-ins come from managed devices (compliant or hybrid Azure AD joined) to ensure security controls are applied.') } $TestParams = @{ diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21941.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21941.ps1 index acf30fce0c67..15892a8feae1 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21941.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21941.ps1 @@ -129,23 +129,23 @@ function Invoke-CippTestZTNA21941 { # Build result markdown if ($Status -eq 'Passed') { - $ResultMarkdown = "✅ **Pass**: Token protection policies are properly configured for Windows devices.`n`n" - $ResultMarkdown += "Token protection binds authentication tokens to devices, making stolen tokens unusable on other devices.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("✅ **Pass**: Token protection policies are properly configured for Windows devices.`n`n") + $null = $ResultMarkdown.Append("Token protection binds authentication tokens to devices, making stolen tokens unusable on other devices.`n`n") } else { if ($TokenProtectionPolicies.Count -eq 0) { - $ResultMarkdown = "❌ **Fail**: No token protection policies found for Windows devices.`n`n" - $ResultMarkdown += "Without token protection, authentication tokens can be stolen and replayed from other devices.`n`n" - $ResultMarkdown += '[Create token protection policies](https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/ConditionalAccessBlade/~/Policies)' + $ResultMarkdown = [System.Text.StringBuilder]::new("❌ **Fail**: No token protection policies found for Windows devices.`n`n") + $null = $ResultMarkdown.Append("Without token protection, authentication tokens can be stolen and replayed from other devices.`n`n") + $null = $ResultMarkdown.Append('[Create token protection policies](https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/ConditionalAccessBlade/~/Policies)') } else { - $ResultMarkdown = "❌ **Fail**: Token protection policies exist but are not properly configured.`n`n" - $ResultMarkdown += "Policies must target users and include both Office 365 and Microsoft Graph applications.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("❌ **Fail**: Token protection policies exist but are not properly configured.`n`n") + $null = $ResultMarkdown.Append("Policies must target users and include both Office 365 and Microsoft Graph applications.`n`n") } } if ($TokenProtectionPolicies.Count -gt 0) { - $ResultMarkdown += "## Token protection policies`n`n" - $ResultMarkdown += "| Policy Name | State | Has Users | Has Required Apps | Status |`n" - $ResultMarkdown += "| :---------- | :---- | :-------- | :---------------- | :----- |`n" + $null = $ResultMarkdown.Append("## Token protection policies`n`n") + $null = $ResultMarkdown.Append("| Policy Name | State | Has Users | Has Required Apps | Status |`n") + $null = $ResultMarkdown.Append("| :---------- | :---- | :-------- | :---------------- | :----- |`n") foreach ($policy in $TokenProtectionPolicies) { $stateIcon = if ($policy.State -eq 'enabled') { '✅' } else { '❌' } @@ -153,10 +153,10 @@ function Invoke-CippTestZTNA21941 { $appsIcon = if ($policy.HasRequiredApps) { '✅' } else { '❌' } $statusIcon = if ($policy.Status -eq 'Pass') { '✅' } else { '❌' } - $ResultMarkdown += "| $($policy.Name) | $stateIcon $($policy.State) | $usersIcon | $appsIcon | $statusIcon $($policy.Status) |`n" + $null = $ResultMarkdown.Append("| $($policy.Name) | $stateIcon $($policy.State) | $usersIcon | $appsIcon | $statusIcon $($policy.Status) |`n") } - $ResultMarkdown += "`n[Review policies](https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/ConditionalAccessBlade/~/Policies)" + $null = $ResultMarkdown.Append("`n[Review policies](https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/ConditionalAccessBlade/~/Policies)") } $TestParams = @{ diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21953.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21953.ps1 index 2a8f7db6e09a..e302b970ce13 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21953.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21953.ps1 @@ -44,11 +44,11 @@ function Invoke-CippTestZTNA21953 { $Status = if ($LapsEnabled) { 'Passed' } else { 'Failed' } if ($Status -eq 'Passed') { - $ResultMarkdown = "✅ **Pass**: LAPS is deployed. Your organization can automatically manage and rotate local administrator passwords on all Entra joined and hybrid Entra joined Windows devices.`n`n" - $ResultMarkdown += '[Learn more](https://entra.microsoft.com/#view/Microsoft_AAD_Devices/DevicesMenuBlade/~/DeviceSettings/menuId/)' + $ResultMarkdown = [System.Text.StringBuilder]::new("✅ **Pass**: LAPS is deployed. Your organization can automatically manage and rotate local administrator passwords on all Entra joined and hybrid Entra joined Windows devices.`n`n") + $null = $ResultMarkdown.Append('[Learn more](https://entra.microsoft.com/#view/Microsoft_AAD_Devices/DevicesMenuBlade/~/DeviceSettings/menuId/)') } else { - $ResultMarkdown = "❌ **Fail**: LAPS is not deployed. Local administrator passwords may be weak, shared, or unchanged, increasing security risk.`n`n" - $ResultMarkdown += '[Deploy LAPS](https://entra.microsoft.com/#view/Microsoft_AAD_Devices/DevicesMenuBlade/~/DeviceSettings/menuId/)' + $ResultMarkdown = [System.Text.StringBuilder]::new("❌ **Fail**: LAPS is not deployed. Local administrator passwords may be weak, shared, or unchanged, increasing security risk.`n`n") + $null = $ResultMarkdown.Append('[Deploy LAPS](https://entra.microsoft.com/#view/Microsoft_AAD_Devices/DevicesMenuBlade/~/DeviceSettings/menuId/)') } $TestParams = @{ diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21954.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21954.ps1 index 763f98070216..f9e6678d5bcb 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21954.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21954.ps1 @@ -43,11 +43,11 @@ function Invoke-CippTestZTNA21954 { $Status = if ($IsRestricted) { 'Passed' } else { 'Failed' } if ($Status -eq 'Passed') { - $ResultMarkdown = "✅ **Pass**: Non-admin users cannot read BitLocker recovery keys, reducing the risk of unauthorized access.`n`n" - $ResultMarkdown += '[Review settings](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/PoliciesTemplateBlade)' + $ResultMarkdown = [System.Text.StringBuilder]::new("✅ **Pass**: Non-admin users cannot read BitLocker recovery keys, reducing the risk of unauthorized access.`n`n") + $null = $ResultMarkdown.Append('[Review settings](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/PoliciesTemplateBlade)') } else { - $ResultMarkdown = "❌ **Fail**: Non-admin users can read BitLocker recovery keys for their own devices, which may allow unauthorized access.`n`n" - $ResultMarkdown += '[Restrict access](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/PoliciesTemplateBlade)' + $ResultMarkdown = [System.Text.StringBuilder]::new("❌ **Fail**: Non-admin users can read BitLocker recovery keys for their own devices, which may allow unauthorized access.`n`n") + $null = $ResultMarkdown.Append('[Restrict access](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/PoliciesTemplateBlade)') } $TestParams = @{ diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21955.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21955.ps1 index ad25a84468ac..2d672a23bf97 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21955.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21955.ps1 @@ -43,11 +43,11 @@ function Invoke-CippTestZTNA21955 { $Status = if ($GlobalAdminsEnabled) { 'Passed' } else { 'Failed' } if ($Status -eq 'Passed') { - $ResultMarkdown = "✅ **Pass**: Global Administrators are automatically added as local administrators on Entra joined devices.`n`n" - $ResultMarkdown += '[Review settings](https://entra.microsoft.com/#view/Microsoft_AAD_Devices/DevicesMenuBlade/~/DeviceSettings/menuId/)' + $ResultMarkdown = [System.Text.StringBuilder]::new("✅ **Pass**: Global Administrators are automatically added as local administrators on Entra joined devices.`n`n") + $null = $ResultMarkdown.Append('[Review settings](https://entra.microsoft.com/#view/Microsoft_AAD_Devices/DevicesMenuBlade/~/DeviceSettings/menuId/)') } else { - $ResultMarkdown = "❌ **Fail**: Global Administrators are not automatically added as local administrators, which may limit emergency access capabilities.`n`n" - $ResultMarkdown += '[Configure settings](https://entra.microsoft.com/#view/Microsoft_AAD_Devices/DevicesMenuBlade/~/DeviceSettings/menuId/)' + $ResultMarkdown = [System.Text.StringBuilder]::new("❌ **Fail**: Global Administrators are not automatically added as local administrators, which may limit emergency access capabilities.`n`n") + $null = $ResultMarkdown.Append('[Configure settings](https://entra.microsoft.com/#view/Microsoft_AAD_Devices/DevicesMenuBlade/~/DeviceSettings/menuId/)') } $TestParams = @{ diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21964.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21964.ps1 index 3e1fbc1e140a..1f6860ca32a8 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21964.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21964.ps1 @@ -18,16 +18,16 @@ function Invoke-CippTestZTNA21964 { $BuiltInStrengths = @($AuthStrengths | Where-Object { $_.policyType -eq 'builtIn' }) $CustomStrengths = @($AuthStrengths | Where-Object { $_.policyType -eq 'custom' }) - $ResultMarkdown = "## Authentication Strength Policies`n`n" - $ResultMarkdown += "Found $($AuthStrengths.Count) authentication strength policies ($($BuiltInStrengths.Count) built-in, $($CustomStrengths.Count) custom).`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("## Authentication Strength Policies`n`n") + $null = $ResultMarkdown.Append("Found $($AuthStrengths.Count) authentication strength policies ($($BuiltInStrengths.Count) built-in, $($CustomStrengths.Count) custom).`n`n") if ($CustomStrengths.Count -gt 0) { - $ResultMarkdown += "### Custom Authentication Strengths`n`n" - $ResultMarkdown += "| Name | Combinations |`n" - $ResultMarkdown += "| :--- | :---------- |`n" + $null = $ResultMarkdown.Append("### Custom Authentication Strengths`n`n") + $null = $ResultMarkdown.Append("| Name | Combinations |`n") + $null = $ResultMarkdown.Append("| :--- | :---------- |`n") foreach ($strength in $CustomStrengths) { $combinations = if ($strength.allowedCombinations) { $strength.allowedCombinations.Count } else { 0 } - $ResultMarkdown += "| $($strength.displayName) | $combinations methods |`n" + $null = $ResultMarkdown.Append("| $($strength.displayName) | $combinations methods |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA22124.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA22124.ps1 index 7ac318bbd035..1414851d8a18 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA22124.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA22124.ps1 @@ -48,22 +48,22 @@ function Invoke-CippTestZTNA22124 { $Status = if ($HighPriorityIssues.Count -eq 0) { 'Passed' } else { 'Failed' } if ($Status -eq 'Passed') { - $ResultMarkdown = "✅ **Pass**: All high priority Entra recommendations have been addressed.`n`n" - $ResultMarkdown += '[View recommendations](https://entra.microsoft.com/#view/Microsoft_Azure_SecureScore/OverviewBlade)' + $ResultMarkdown = [System.Text.StringBuilder]::new("✅ **Pass**: All high priority Entra recommendations have been addressed.`n`n") + $null = $ResultMarkdown.Append('[View recommendations](https://entra.microsoft.com/#view/Microsoft_Azure_SecureScore/OverviewBlade)') } else { - $ResultMarkdown = "❌ **Fail**: There are $($HighPriorityIssues.Count) high priority recommendation(s) that have not been addressed.`n`n" - $ResultMarkdown += "## Outstanding high priority recommendations`n`n" - $ResultMarkdown += "| Display Name | Status | Insights |`n" - $ResultMarkdown += "| :----------- | :----- | :------- |`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("❌ **Fail**: There are $($HighPriorityIssues.Count) high priority recommendation(s) that have not been addressed.`n`n") + $null = $ResultMarkdown.Append("## Outstanding high priority recommendations`n`n") + $null = $ResultMarkdown.Append("| Display Name | Status | Insights |`n") + $null = $ResultMarkdown.Append("| :----------- | :----- | :------- |`n") foreach ($issue in $HighPriorityIssues) { $displayName = if ($issue.displayName) { $issue.displayName } else { 'N/A' } $status = if ($issue.status) { $issue.status } else { 'N/A' } $insights = if ($issue.insights) { $issue.insights } else { 'N/A' } - $ResultMarkdown += "| $displayName | $status | $insights |`n" + $null = $ResultMarkdown.Append("| $displayName | $status | $insights |`n") } - $ResultMarkdown += "`n[Address recommendations](https://entra.microsoft.com/#view/Microsoft_Azure_SecureScore/OverviewBlade)" + $null = $ResultMarkdown.Append("`n[Address recommendations](https://entra.microsoft.com/#view/Microsoft_Azure_SecureScore/OverviewBlade)") } $TestParams = @{ diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA22659.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA22659.ps1 index 05f1985ddb04..2ae158e97554 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA22659.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA22659.ps1 @@ -48,13 +48,13 @@ function Invoke-CippTestZTNA22659 { $Status = if ($RiskySignIns.Count -eq 0) { 'Passed' } else { 'Failed' } if ($Status -eq 'Passed') { - $ResultMarkdown = "✅ **Pass**: No risky workload identity sign-ins detected or all have been triaged.`n`n" - $ResultMarkdown += '[View identity protection](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/IdentityProtectionMenuBlade/~/RiskyServicePrincipals)' + $ResultMarkdown = [System.Text.StringBuilder]::new("✅ **Pass**: No risky workload identity sign-ins detected or all have been triaged.`n`n") + $null = $ResultMarkdown.Append('[View identity protection](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/IdentityProtectionMenuBlade/~/RiskyServicePrincipals)') } else { - $ResultMarkdown = "❌ **Fail**: There are $($RiskySignIns.Count) risky workload identity sign-in(s) that require investigation.`n`n" - $ResultMarkdown += "## Risky service principal sign-ins`n`n" - $ResultMarkdown += "| Service Principal | App ID | Risk State | Risk Level | Last Updated |`n" - $ResultMarkdown += "| :---------------- | :----- | :--------- | :--------- | :----------- |`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("❌ **Fail**: There are $($RiskySignIns.Count) risky workload identity sign-in(s) that require investigation.`n`n") + $null = $ResultMarkdown.Append("## Risky service principal sign-ins`n`n") + $null = $ResultMarkdown.Append("| Service Principal | App ID | Risk State | Risk Level | Last Updated |`n") + $null = $ResultMarkdown.Append("| :---------------- | :----- | :--------- | :--------- | :----------- |`n") foreach ($signin in $RiskySignIns) { $spName = if ($signin.servicePrincipalDisplayName) { $signin.servicePrincipalDisplayName } else { 'N/A' } @@ -73,10 +73,10 @@ function Invoke-CippTestZTNA22659 { } } - $ResultMarkdown += "| $spName | $appId | $riskState | $riskLevel | $lastUpdated |`n" + $null = $ResultMarkdown.Append("| $spName | $appId | $riskState | $riskLevel | $lastUpdated |`n") } - $ResultMarkdown += "`n[Investigate and remediate](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/IdentityProtectionMenuBlade/~/RiskyServicePrincipals)" + $null = $ResultMarkdown.Append("`n[Investigate and remediate](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/IdentityProtectionMenuBlade/~/RiskyServicePrincipals)") } $TestParams = @{ diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24570.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24570.ps1 index 7e8b90173796..68e6c82a6487 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24570.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24570.ps1 @@ -137,21 +137,21 @@ function Invoke-CippTestZTNA24570 { } if ($Status -eq 'Passed') { - $ResultMarkdown = "✅ **Pass**: Hybrid identity is enabled and using a service principal for synchronization.`n`n" - $ResultMarkdown += "**Last Sync**: $lastSyncDate`n`n" - $ResultMarkdown += '[Review configuration](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/RolesManagementMenuBlade/~/AllRoles)' + $ResultMarkdown = [System.Text.StringBuilder]::new("✅ **Pass**: Hybrid identity is enabled and using a service principal for synchronization.`n`n") + $null = $ResultMarkdown.Append("**Last Sync**: $lastSyncDate`n`n") + $null = $ResultMarkdown.Append('[Review configuration](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/RolesManagementMenuBlade/~/AllRoles)') } else { - $ResultMarkdown = "❌ **Fail**: Hybrid identity is enabled but using $($EnabledUsers.Count) enabled user account(s) for synchronization.`n`n" - $ResultMarkdown += "**Last Sync**: $lastSyncDate`n`n" - $ResultMarkdown += "## Directory Synchronization Accounts role members`n`n" - $ResultMarkdown += "| Display Name | User Principal Name | Enabled |`n" - $ResultMarkdown += "| :----------- | :------------------ | :------ |`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("❌ **Fail**: Hybrid identity is enabled but using $($EnabledUsers.Count) enabled user account(s) for synchronization.`n`n") + $null = $ResultMarkdown.Append("**Last Sync**: $lastSyncDate`n`n") + $null = $ResultMarkdown.Append("## Directory Synchronization Accounts role members`n`n") + $null = $ResultMarkdown.Append("| Display Name | User Principal Name | Enabled |`n") + $null = $ResultMarkdown.Append("| :----------- | :------------------ | :------ |`n") foreach ($user in $EnabledUsers) { - $ResultMarkdown += "| $($user.DisplayName) | $($user.UserPrincipalName) | ✅ Yes |`n" + $null = $ResultMarkdown.Append("| $($user.DisplayName) | $($user.UserPrincipalName) | ✅ Yes |`n") } - $ResultMarkdown += "`n[Migrate to service principal](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/RolesManagementMenuBlade/~/AllRoles)" + $null = $ResultMarkdown.Append("`n[Migrate to service principal](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/RolesManagementMenuBlade/~/AllRoles)") } $TestParams = @{ diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24572.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24572.ps1 index 637e96170614..0630bcb09e7a 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24572.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24572.ps1 @@ -24,19 +24,19 @@ function Invoke-CippTestZTNA24572 { $Passed = $AssignedNotifications.Count -gt 0 if ($Passed) { - $ResultMarkdown = "✅ At least one device enrollment notification is configured and assigned.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("✅ At least one device enrollment notification is configured and assigned.`n`n") } else { - $ResultMarkdown = "❌ No device enrollment notification is configured or assigned in Intune.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("❌ No device enrollment notification is configured or assigned in Intune.`n`n") } if ($EnrollmentNotifications.Count -gt 0) { - $ResultMarkdown += "## Device Enrollment Notifications`n`n" - $ResultMarkdown += "| Policy Name | Assigned |`n" - $ResultMarkdown += "| :---------- | :------- |`n" + $null = $ResultMarkdown.Append("## Device Enrollment Notifications`n`n") + $null = $ResultMarkdown.Append("| Policy Name | Assigned |`n") + $null = $ResultMarkdown.Append("| :---------- | :------- |`n") foreach ($policy in $EnrollmentNotifications) { $assigned = if ($policy.assignments -and $policy.assignments.Count -gt 0) { '✅ Yes' } else { '❌ No' } - $ResultMarkdown += "| $($policy.displayName) | $assigned |`n" + $null = $ResultMarkdown.Append("| $($policy.displayName) | $assigned |`n") } } diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24824.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24824.ps1 index bb08a44adbd4..d42e8ad8602a 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24824.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24824.ps1 @@ -118,9 +118,9 @@ function Invoke-CippTestZTNA24824 { # Build result markdown if ($Status -eq 'Passed') { - $ResultMarkdown = "✅ **Pass**: Conditional Access policies block noncompliant devices across all platforms.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("✅ **Pass**: Conditional Access policies block noncompliant devices across all platforms.`n`n") } else { - $ResultMarkdown = "❌ **Fail**: Conditional Access policies do not cover all device platforms.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("❌ **Fail**: Conditional Access policies do not cover all device platforms.`n`n") $missingPlatforms = [System.Collections.Generic.List[string]]::new() foreach ($key in $PlatformCoverage.Keys) { if (-not $PlatformCoverage[$key]) { @@ -128,19 +128,19 @@ function Invoke-CippTestZTNA24824 { } } if ($missingPlatforms.Count -gt 0) { - $ResultMarkdown += "**Missing platform coverage**: $($missingPlatforms -join ', ')`n`n" + $null = $ResultMarkdown.Append("**Missing platform coverage**: $($missingPlatforms -join ', ')`n`n") } } - $ResultMarkdown += "## Compliant device policies`n`n" - $ResultMarkdown += "| Policy Name | Platforms |`n" - $ResultMarkdown += "| :---------- | :-------- |`n" + $null = $ResultMarkdown.Append("## Compliant device policies`n`n") + $null = $ResultMarkdown.Append("| Policy Name | Platforms |`n") + $null = $ResultMarkdown.Append("| :---------- | :-------- |`n") foreach ($detail in $PolicyDetails) { - $ResultMarkdown += "| $($detail.Name) | $($detail.Platforms) |`n" + $null = $ResultMarkdown.Append("| $($detail.Name) | $($detail.Platforms) |`n") } - $ResultMarkdown += "`n[Review policies](https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/ConditionalAccessBlade/~/Policies)" + $null = $ResultMarkdown.Append("`n[Review policies](https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/ConditionalAccessBlade/~/Policies)") $TestParams = @{ TestId = 'ZTNA24824' diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24827.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24827.ps1 index 7a6e0bd833c9..6f5413e480c4 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24827.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24827.ps1 @@ -128,9 +128,9 @@ function Invoke-CippTestZTNA24827 { # Build result markdown if ($Status -eq 'Passed') { - $ResultMarkdown = "✅ **Pass**: Conditional Access policies block unmanaged apps on both iOS and Android platforms.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("✅ **Pass**: Conditional Access policies block unmanaged apps on both iOS and Android platforms.`n`n") } else { - $ResultMarkdown = "❌ **Fail**: Conditional Access policies do not cover all mobile platforms.`n`n" + $ResultMarkdown = [System.Text.StringBuilder]::new("❌ **Fail**: Conditional Access policies do not cover all mobile platforms.`n`n") $missingPlatforms = [System.Collections.Generic.List[string]]::new() if (-not $PlatformCoverage['iOS']) { $missingPlatforms.Add('iOS') @@ -139,19 +139,19 @@ function Invoke-CippTestZTNA24827 { $missingPlatforms.Add('android') } if ($missingPlatforms.Count -gt 0) { - $ResultMarkdown += "**Missing platform coverage**: $($missingPlatforms -join ', ')`n`n" + $null = $ResultMarkdown.Append("**Missing platform coverage**: $($missingPlatforms -join ', ')`n`n") } } - $ResultMarkdown += "## Compliant application policies`n`n" - $ResultMarkdown += "| Policy Name | Platforms |`n" - $ResultMarkdown += "| :---------- | :-------- |`n" + $null = $ResultMarkdown.Append("## Compliant application policies`n`n") + $null = $ResultMarkdown.Append("| Policy Name | Platforms |`n") + $null = $ResultMarkdown.Append("| :---------- | :-------- |`n") foreach ($detail in $PolicyDetails) { - $ResultMarkdown += "| $($detail.Name) | $($detail.Platforms) |`n" + $null = $ResultMarkdown.Append("| $($detail.Name) | $($detail.Platforms) |`n") } - $ResultMarkdown += "`n[Review policies](https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/ConditionalAccessBlade/~/Policies)" + $null = $ResultMarkdown.Append("`n[Review policies](https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/ConditionalAccessBlade/~/Policies)") $TestParams = @{ TestId = 'ZTNA24827' diff --git a/Shared/CIPPSharp/CIPPTestDataCache.cs b/Shared/CIPPSharp/CIPPTestDataCache.cs index 9f0f639a1d00..496a631922d9 100644 --- a/Shared/CIPPSharp/CIPPTestDataCache.cs +++ b/Shared/CIPPSharp/CIPPTestDataCache.cs @@ -103,25 +103,38 @@ public static void Set(string key, object? value) if (_cache.ContainsKey(key)) RemoveEntry(key); - // Evict LRU entries until we have room - while (Interlocked.Read(ref _currentBytes) + sizeBytes > _maxBytes) + // Take the LRU lock once for both eviction and insertion. The previous + // implementation took _lruLock per eviction iteration plus another time + // for the AddLast, which under write contention turned every Set() into + // a stream of short critical sections instead of one bounded one. + var entry = new CacheEntry(value, sizeBytes, DateTime.UtcNow + _ttl); + lock (_lruLock) { - string? victim; - lock (_lruLock) + while (_currentBytes + sizeBytes > _maxBytes) { if (_lruOrder.First == null) break; - victim = _lruOrder.First.Value; + var victimKey = _lruOrder.First.Value; + if (_cache.TryRemove(victimKey, out var victim)) + { + Interlocked.Add(ref _currentBytes, -victim.SizeBytes); + if (_lruIndex.TryGetValue(victimKey, out var vnode)) + { + _lruOrder.Remove(vnode); + _lruIndex.Remove(victimKey); + } + Interlocked.Increment(ref _evictions); + } + else + { + // Defensive: dictionary already pruned, drop the LRU node anyway. + _lruOrder.RemoveFirst(); + _lruIndex.Remove(victimKey); + } } - RemoveEntry(victim); - Interlocked.Increment(ref _evictions); - } - var entry = new CacheEntry(value, sizeBytes, DateTime.UtcNow + _ttl); - if (_cache.TryAdd(key, entry)) - { - Interlocked.Add(ref _currentBytes, sizeBytes); - lock (_lruLock) + if (_cache.TryAdd(key, entry)) { + Interlocked.Add(ref _currentBytes, sizeBytes); var node = _lruOrder.AddLast(key); _lruIndex[key] = node; } diff --git a/Shared/CIPPSharp/bin/CIPPSharp.dll b/Shared/CIPPSharp/bin/CIPPSharp.dll index 79dae0140c352bbf55f95f7c5f9466d8def44454..92d07b054ef993dece27dd1fc7244bf175c1ed68 100644 GIT binary patch literal 41472 zcmeIbdwi7Doj?A0o|$>(lFTGClPkH90KtJ=NC3GgD1mTOB1nROw=g6F3?%b}nS_gw zkSJ=cqNP>qt*x|bwXJP!tF5;c+UnM=*4ozXx>XjccGq3o+U<5%w_bkl&*wbPToSO| z?e6cje|!h#^L%dSb3W&D&iS14oOx!_e(@*BK}1fxKm3sBJGk;|k-(1zBM=8BJ{F*F zd7rQSj<)Lg+Rkn9R84=<+?tH`)pSP_33H%kbF3ygn5c;-YT8zJ)byD>u_nLYH^nl& zW+~As%|XMzxacXnwi7g|CQoZ7x&s_Talgur)!=;;Z=yoMb>%lR*njoXh`{HUgEn2w zs{B85wMb^+bt&wwX5=VQKL=v$>jY6gc#EJ{Iac;jbPbUwOYQ-^B8zSsi0v5wee^Z} z$fT{7-=O4GL$s+WnM!tp65oyj;fCIYch+kW+-pk4dQB*@uXF-$wskk&S+7Mzi?c{U zKf!Uui+$6{Mc=uQ=wn-m5XJx2t6JYzLgbr{YbjqZ(j#TK>W(31>&_wJ5`~)`)QTEV zPtY+4SmD!+rrAcrj7T{viWNhkIRT7@(ny8yxx68^P4tIvddc+C=|8FIJCJ{G&9rsv z?!7pxxLE~-4Hi37+N@^wboRkbo8bdO6)Z}^hyJod0k~9GqN?|WsBS0XU2y}0M(5O` z`n(TH7&j-v#OLcuRU`94THQ_;>oKtP7*~3c8g>*BskN??tm|a!I>oxyS=Xt2{Yg4< zTnv%pQo3u13+Vyp5Es$|t|2a@2aF*pBys&B+T@V98t?&eJ^Rz(81ruew8FzC4e>`s zcefQ?uEW^JP@>z=6*+^=@8e1hfo23~!}7>9;jO=3<3!j}b_AxXYm|q%c z6h1p?>|6o6LblG*co!x?ew!E16z@COR++i{s_B5uDYEV{uMVc zuoW2}Gm2-SIXs$Y^EETSnT=5@+|2xT$v7;4l`-?1)s1}&bLTggH^g@K{4RRvmBl%u zLJE$b-(31M^Lw6_J-@m9pVN12@K5z!dVZe?Wsb}o>pIuE&a0rwDF)B~O&vB)|0RoFuoK#s*CD7oNy z=FEQ@(8>i|gRzgH^u+uxu}Gy$uGNs{NN1=Fq9z)9V(eq6GQw^vBd}-2K87kIbXggJ zJvH_*R2gBpl@Zu;V;@7w2*@A{f$RIYC5Axr9H7V`=L(-&X6$2VE&@;)M2)wO+B80gNR!6r5NVO*5X*L^S!OHhwewS^Rpw%5 zTkFzA$T@CVm#)CLtxo40ZpoiBZnnbKNpRiBd};uPZi}@dc|Z zzs^|{Sq{>~UM$Tci?@<_rRI6+az0-s`pi{wtv1_nOHWk7yl2`S7J&$Pg@b^(anEJJ*V!s|5Nq-&-Yu|NA}zQzwy)w z>74J!f+33F4#zi?!psa&@laB(`xO~M(yb4XP?3)G9J)v+@}jJs>g36_4#4MV3>e9q zbn34#F9O{V9L9?2Onwg>BekB38p)T_Vpg8$VC4-;dBRxbJa;mlQ79QMV}--uC~Nui ze##3%@-w0%HJ(D}Yohbl*>(2=Gx@_Tg_-W;Pd`#2IE))dXY$=l8IP>@ z4#GGjACEDgk@OnrMmS6J>Mu2tExDZYjO4mp&O9TzFPAgbNIsd%DKe7(k;~Cb^4wCt zi!MR^{2H!>gL>)qVI)>(U1RCg;f+8(Fb*RoDWJZ(5tf($zNy2w!E-vGjMCZ4pUfz; zL@D!n+`)?Nu3Yt7Gqzb`H6+~ZA@n2Rt}LiKoUVW~;0hQ4H}6~x z6Hf3BEdu+p2J3|6!sjxadtk*kv%y>n#_t@auq+5vBMHo*VbH}&t`O3GQ2HU{JBhBEKzk$Va0f^;@ufLEKzlB z&(%4S(P7E(j$HBQGj*{<)x|SyxHnhv3mHY0L|h!~zFeJqGIg;eg2wqG1{5cjN8GHR z$ko4>_2Cu9081iRGwUN*{OX5!Mh%;}+V`_VF*5g#k|=jwcsbzloREE(RJE54t_ zC=4-6BA8V5DJc3k1Sj6 z1dNEy5Cn{AHbao}th)%7TDK12W`jnQ%ap8jV{tgNR2@^+H5yapro%+e@R>*;d@AWU zCA-Yo=uXz5biFG%AD7S9mAg)$F6fJQ?t^~1tW1f3vi)F@P zUIo_F1I_8mPFL@NhO~Oh+U={7a`9n!W&m$PJh zvu3p<^-V76Fu%$dr}=fhi1VE8*?IFDV3`Mj9cW6spzgykPN*Plzri+%;Bc!N@(#?% z6sW-c28{5>7D2I(8*j1iE6FR-B{KOE6!U774Lj?X2A$?LjJn*8nfmc|Bjk!)3tD37 z*}ie+6j2BFW8j7iRcC>_bVFWNIAP>Eh#S9gJU{4caD)6f$XDFcH8`Tv6k5AWeZ0V9 z0D+U2H3syO>BIGCfj)dApsXo}fP7#)f{3K9hn>0+mY6pHJQo zFegT05-~ziUqilO-h}Ja!`2^P{qv0@PGEN1P z?h#DlE?6^rC0L7%Nt;Y;XMCwoG04tFe$Ga6X17{ZNm5hE_6T+)7^hrWn&A4<1Xr0R zxYjhm72Cx0VU<^~W$}&hOj6{nOmbYNU!af7#MRLo!NTnunZ-PbBicYA#PPsHN`}t_={2#?`YaSn zsqeVY(_pwGSTTLC95@RaR#zJ4w_)_c(&MTsdc$pE`Nb`l5sAWRzGs-5-GV73jWX|s(5}(pi5vkr%)y9! z9$4ye6!A+t!_JbhE9k^5;HJ*Q*dS?@`2~>kKbb?C_kb5Rq=ujoa+~*pZdiF>9_$9) z=67H;7P$|+sS+}%cvQ^F#bnT$6g{r<(ReV4AR(Yi}%=SGSq=5^& zKK$@Q)kLrP09QX%s`ps+a^L9YmmmmxRlfzj=9fWY`nq`fsvbhn)%@+DH&$x-Xr+vi z9$a~hqz8sHuv2O{Cg1f*brdx6GP^4Ln z#U=>`=XKxoCW(ZkgXzf(-izg*U(KzTltmt5#R<|`(`O!#A@>ci;wUWB0`rg!$t19F z83Tg<`!MSavv32}7@uXBXu1gOiZ(8~I7_kjl2TPG`cS98h9kus!*gJSmz0ILX2(+a zu2(S+aH9WBcw>GGAiu<4Kj`t^5q1V~dyJic`7pSc8m^+qBfuIvgRYTtVJhfK9%3=3 zwQpn*Gn~mUr1g!UQ53;M@HI3AjpQHGimj|@BwZf6*$7u$PvJ>)v*G8%Sz-J1$RwQEN0x zwufEEXS$0b@TdO9?T+miD+Ajv0e^z73>QTTL<*LBs+6Fhi{~BMqtjHHwwi}Jzw+qt zUF7Drao)pPNTuAK2B%{}i>^-}=BQgiEfzvIpMtv$9^Db?MiZu|+qzC4TnaOd?fPH~ zW_07y+_smb+wLgckZHZu=wF_u(bIKnuCAp?1Ip(L?2Ry}vC2{~brin%h8KyG$AC9Z z80OwCiU{E!=AKqWgO2SVE9&NVp(s6sY!0s5>YX;rL$Tl$tvWfbcgaD$bVr2|bc#_W zPdDXzFe#(RYR|XQ?ZF^MC%V+m#jyH-XdRt(lRc(Z(VDwd+j!r{_r~RDn=Bh@nKZx8 z%`DlTmwFmVz7ckpggx_~LlA=QpyvxnP!(Zs$QyYE(uw=aN=(c{U)UG)-54)zb|-@v z`1Q`9uk?;^UP(9~ir6pvWNnm1E$k2a8@xfkENZ#(V7{zy;eb0B*pTN5=7kE(XHmGZ zJar>_zb=@^+dLy&08y~O{2PdZ1$E}N%qt8QmToYyzcZMHjhq1+90#GV=WG53-hLTD zPw9rRC+G>fZ;YQi4U0^PzXTuIxIQRG#;ph$>S{F`%R*MOgQ1{m4W^RO;0Zcm$yx8@ zxot}@1&weBi|h~KUBi@MC>##D!bPDXVeon$2!=r+p1#QQtm6&2f}zNdfQF3hQpV{@ zy`V}NR;ir!tU-HH*gRv0e`DO-%81vD^9B3a9ej1$sWqL$%|7NwHUgR^qmt6ctL z8SsWV!{vOLF$_D-hR86~oDH>d!rw4KPV;f7U(L*XIfid=z535w7}LaQzKS|Z%;SK3 z67dhX;?wiU30xb`)l2Gp@KAtnnCCl!y3oU)V1< zkP`5!@Gb>@23j^p1;7z`gDr5sI4KE5IE#K6gj3jztujhCmM(b-MIy*2hKenJ;4i`D zM+8)KTP3<&NDn%PuIB6hU4S7B`7npK0+#iCEC@ry*R1$*IauQO>iWLjpm4Z7bO2Y7 zd-$@CiO5{IFf_vSeun%05e4!s{GYh6V!sJAG9S7qy%%~Z3=lkv!iMCtztq2g9vTHn z(!7fiI(1wA76g8M-xUxwY*SbB*WiVX`UP0le#0m>4Iy7BFY;RuL0{M%%nN(;eQeEq zn_1Xhn7;$k=<^ExufS3E_sn|ycnJOM$Ld^yMWjM)aP)oqP9=L z&R2@`5zY_hPdvNK{5P1XE4d@=kNg2xI8YiX;)UZsm?)@^Vn6W?qu56z*P~d4`7X$Y zmBGSro-iW+$>I=p9(bEf1VbCmKjK<`=yHsTx*u;~58eaAe4kZ=!C>SAMgqt4rylpB z0;D-#<7hirs2j=h9O18V93$v9jy9l)lZQWVUW2u}6d z!QYb|{JfA)1wRj)5?{DTnFtpLi%qVfu{>BTr>PR56dwEY)rCt==|?bpYA1r>V3G2` z>bSG^V?%PvzSH*&q0uGHj-qQzHfx z>Rk~x>tWp&y*1WI()TgP^gvc(dI7`bq4J22k+M)(1k>Bs*s7NzHK{3AHiR9eJ>|;u zNHI^j`yD)>SP(AL_w)6G;>8$slG0?#l|}N|WT_r39l~zQc|1HdYc@sB#uWNs0(@rK z(6s<$lr<$ShUrJyA#7FA<*Z-^KsW5qBCJ+mV51dQHqg1O%-pOShtW z2t&(m1=ht9ZpAM#IrV)k2v_U-`T9Ze(wtUQN5X8w;!s$BYjyXl);s>|O#LA2P^aOq; z5!Why1`*c^e&*1~d-X=lB&5->zK_RbL!P>-o>6?BWX$JlHlNcF^Cca99O>zm@DyH8 zWftw{HRXe1jJgWFNR|g&*n+;FXQSLuX<)INYIO@viJSpHrx?Bui`@kTQL_g z)(O*sR^oaIuGpERU-=N`fhxrnGn)duJHhqa&`aQ22f|+SnUBX>2*ZO)p`2}J5Hy-V z@8XK3Ic?{J69;0T3thQ(+CVdZU<|(;`q!b3&l_^C01Zr3yxU>75AVmu@ZW^&P1yKJ z7N5&tV?eKg_s@7Q1Ad0uoABFj({>aV!kPq7ud+G>Z~+E1s&9p^39utM@HAMahWo0^+uHP33n?uGC@j$Xhr zjE%`i1qX3`ZxZm1fn+?fHN_euxNq`!!CTisAG=8P+vMfzR<=QQ8|b$SAY0sPZf0e? z&_CMx&wak$i@`(xsmR`_&T0XdQ=qiD43fw8u7oh2*vmCD)2E9;yU3q{$nD|z4 zi2gQwSEz)V9rxB4^jHB~cv$qW4qsVe&_1#MLkIKc>0HY@8uPy|oCyMJ#pYHi>nUKJ zI|J91f10MExds&#bJ?3puPg7MJB9Pz(9dfPTBg5h1n8W=Tg3r-vxebx;07Jnt}B1a zD*L#`-oA#i27L&=LHGObLS!ZuF}x4$4bdBd=NG*Y_E9Hn`si~~>fvIJ&zFi;R2uY| zNsM0=SW)Sti=kuC4Q0%~DZsvcU1RuD;67vwmi}XiIR{E#2nWbn_Ezx(8YsV^IzT@z zWe=aJ<=X4RtTTevRnx(MyQG?))!Zcpm6kET40a~bcPp!lCsDkt8l|Ry6QY;gT=pH< z579Tv7;k~+2K`;-3*ioW1RGa_uGhKVtNa|3?N$F$8KCn44Y~sU7<6?N$6@Cr#=i-l z1M~}t-g?n|5*|*VfA+JtbA{tAW;=63|H1>6jw9RfxWov=hbOq6Ymy!lqaN^U(d}PYWGzw~@hN&`Y5?Q6l&Y)J2Z40n$I%WiG zUM$p1T8lah>F>%|wv@IAb!RnGopd$3NB=1GZ6F+O))vx_kO$|}My6n8CpJ4^N{dXlgSqWjSHLv#xrrCJ*FEC#&Z zvkY*#m+`+B{I5L^0Ke4xAmHQPhX6n4c^L3VqI18O^&gh9R}20d(cdNdGep1G%lbEa zo3&c{&@&tGS>GeRTDm9yQNWk-_kh!$$M{wGPxylL=)|W1e=qRo0uur!)iD27!IufX z6R?F6;b#FOmCpknn!s?U!27EhpC|lB%U^>1Z}_;D`aG`j4F9W;UNYeX;GT*%VY#CE zRlsjdVEE5f41JXhzg5AoP^1e5KNh}6YoXnNe?{4Emi`g&UuzwCuoEuK3(_RfnO6G< zN@d&pHmtSKCehj8c@)ws3&POqGlwn!-l#D=tX~Fry@PSx z$?zg4bG|G1bQg0>7jwQWxF%)28gsTv*>)*=+|dipOy>yTmCl<04cBM$44PZ`hz~ii z^g;N4oA!+?&bNU-Q^e94;XRr`H~JY4RUHL?_aw%@S-|io2J6fZKZ;iWt3( zD6bH(7_qITwLT-imgWGmPF{WqW?e!4ni|eb*LY%{cI>J*SX5=88h4GG6jgR#*?io) z?h)!4y1n9A!$D75)KULhmy_PJs5e5_VkO>QOL$%5DX(nTU3kJsQ3obeyIgdIMfFUW znD3%1E$Zf~DWE=XQBBp;Kz-7p9w=`Db%&zh%YQmt^kbovFL`tj8HVT?Vqfy9X0oE# zmwalTqH6D}ZP)!Y&!TFpt6hFtq$p{hpEd}kY!=YH7R5FT=@pA&n}u{9CqK0Bweog7 zNNX+X?W$^5kS?~UyU@NMZMLYBXkU=FThu!h(?BIH>h6jrP*+%#UOfxcl@_(FVjid) z6a_!KQNt@jDL>2U2UAsR+0O}dGq$W83HEaW{X1@2*w5!6tDp}o>KFcn`4!}zresf6 zO?FjMfkpkWsx7~g$}H-8HP=!V)mYREHRtD7(KL(NUOd@VO)VBRSlpIhO=nqDLDky) ziL}_FCRbew>U>3^4cIGDuTY2S{)zGY8hXW|UKeT=c9%ql=^3G(u&50}O{rHhZw;5b z*`h{7=4?>1xgr~|sLO?V-J*)z36!g8RGJsoq(B{hXUve>!clsCSHe@@LWsi+Z!{vHY{>q1jySR(hxG zuRy&m)ZwzP2zB|HEW5@N_fK}6MO!Q?sHSnYTgA^W>LY4_d!jusLh7v@1$0XY8KfFi)tw6a_cPW?J6!8wWzx$Fx6*KcUCgB z*P{MVz&5`l)L~lgVPEb)hkZFrBOw=Ld-!$))OU;X{1;KpB1J8(4*HRgg}RmAEUWZi zLO)(2G8qMzP-7cY*LdbnSnIlk&a|lWaQ1TvonukOkX=g4Eb0u%E~V8L_1p5buFGh> zMd^@TMw={Zp0U8VoVHn1yHV@EoOW7N2(m8PYf*KObL$CrtHs(=!(Jhk^x0 z5B)B}}M{W1C{i@Mld@83diTU5V$4ybo6>Ke$llD1Su z_YTOmlFy>NwQF75C}dIPwHod{D=g}8@nV0RCR@}OidTVZP?XH7?R2gsTV8N2ZKvfH zwWgpD6#oVV_vRebu!A;O)Kb*2gQ6Dodqkp_;ufV>O!fCt(xP6hSnKMeD=g{+&Y}9~ zDvL7ED+#(wQQ}L2z9y8KV+ndZNA^9TuA)&|?@!RP7PV1Z@9(Dh@njZ(Gz{t=qqk-nFPZ^@M+j^kp20tLPR-w||)O zEGpmG?LR<87PZNl@Q+fJMLh@El{D3&X1lumSJ4cM+UrXAucmnx^)O`DP^(4d!PdzMQwmD*Hg@*-gNBs-#}Mb)I#TV{zG)oq7FH4_TNZ%TU5~X zIsYfGBrB>i|zM)pZMF4Q$1&gGw?Us)9A@=wuk zEsAsb&Ga7@#km}(AQ!TgYdlSpjyZ23k42pe*)0@Ql#GH~X^K#46x>S9IkI^|U4?HQ ze#w6;wJMosV(4b$)3nl}et{ABX}Zv&7P-If|1@p1s1~83ib8#lKz5xaJL#V6x`XZ% zO4WV`eKDVi|98a_Cr7$4EdOQVIjTh{556Zl+l7od|K ztm4zBnbmxU{$Tl7ecnInR0nCUoeq@M>3x)S(3i!|8)E0Z3bu0(pqmVo@=!1E+_LPC z;*5v3pEGb=snZ6j_geU%(G$YS#$T4&gVIKpYPc^bEYxvdP*~`|9YJBC6L$xNIOP0h zL17`^5fm1>ad%Ky=)rx&e@B13=U>I4f=;bCdvnk$#T?IXgX5yx!O`e;_@Gl%qIx^_ z&^m2|j)Q7|D=G4NIqN(vQBwMk<21>Zo>Rfn^DGNGEGQq8wG6HzsndrwT$cIhHN?b@ z=PZdHbH>N~f1mR=2K)24`1Y6J4nClimqv_d!~ZFMwoQef%5uH*uY%)81-H@eg*P!f z9JHDbpXfQG09mXMZ!g`452AvA2A*cSugpi&01K%_;Cz9t0#^#WP~b*^Q9#UaKm)7J zePzXTo#2N7E9iECF9O!kNx-S7?Y^=Gs+EjdE15K3&VusgENCaBIL)NjsxJe)y<#g) zei^S6ya@MvyMZ&@DR({7^s}pPq}g(UG+R!PW)q(v&6X3S*>Zw3TTYM`h@Ayur%muS z!B+{sO7Kp>I|W}LwX_LbC9qTAemRNSPka(}P)?!_5}!mJBtD5cNPH4?koY9(An{4m zK{<&!C?`=ji2WPH{>_5lEcgv#`DTIB^fz&O@D+)}e4V2)Ute2vm3B;fY{JL2`T8q? z!+>9{Wq3*DZGhd?42SWl+kCwpeen>=enz`ccNX2FJw%^}W~)|S_*K9Swci394Ik4s z!g3*P)cb0$rD?h+{8Q~)DD}EFO~1Y3r`i+1-_yQN{z9+*oZvqb{1sYIworeaW=(9@ z-=gxuEV7LA4z`oD>0Twg!wYW+P6zi-l`dR%`nbhW-$f1%(Hz}HOrBj9kL%HhTGwpt4>fCDNA#oadz_tuUkd$ha4rm711WpkDe#!K zuKeQ~`*w$`NBO2NFT3B>EBJtx2z}Qzpj}@3UDreU@#^PYC&gNic4*SCUFG^~wZ8{^ zx#(T!+)(Q>_QTtDy+_(|M1RqJ4eTUBzlQWy>{fcTAK?(4tyLSn($XG{BRL?jS0H;h z!|0J(9?*yUbBtqJMfn2bQT^j`O@FpJ>ApvL;Vy~EU84CFZB=Np>nqxCtK0Q*{hjdD?ysQKZAQ8NCh)tofgDi zX$#8Uakps2p2@CO?F07*z-LV~s8xHfDClX`77ETg#kYv1-=e6#7-!_m9m@d!&hY@f zEORi5x*qnt0(hnK6-4YA&+8a(fA4uq+gtq`PgHYMz2kXP?<@KMIvknTkvS*3I2umx zh1wRE-y7GTEv|C>8_HID-;=RAO%Gy5Pt)HIt%c@qLNmPY>Rpr0^zw{27p0yGE%gQ+ z_m!>kMzt4e)&YL6=2CAQb#3;(sV73)MB3*)0A=fdv|K@;;iH_JlXF&tbII;^-7!Tnd3XG#Y?ZHHpK8YV?Nq) zp0C{Tg@TKHt&WYjN1LX1N@n{lvKdEXlkY9!etC-)PQ1-{i@0CP_1iE)0{9eT6weDj zL>~iuSm2|8IzF>Fh!t9=ZxPWvoiqxJ=#kIvNY2R>hW2&c6>Xj?%YzSUBme+JF~epS$fPl4tHn(*n* zxquaPIpAd43Rq9~0?wju0iI1y0WP8E0M8eG2VkpMTSDFV%HUG5xrx6+UJw<2x9~@V zGb)^`gmVy@KPb3c_(z2Q1>qkR&N1P95BGLIDEOY(JSm*lg!7tkG>u!~&{)Tzaa(GI zGg&y3h0`jWCBj)EoTzZRh0`sZQQ=%AoU4R$5D>m;+=?S2{es4=J1U%G!ug(XP6+3u za9$G*>0CCcMCi!@PxpV0?EZXn!p-?wE`Cj+$3;B;6WGrd5`N(`cc961wUvA$6(G; z<9c!xd|xw_}2z5d5S_2`6Ca z7o42Hn&7p9FA}`f&z3g{9u@wG;G=@yE%*_^j|zTF@DqZc6r2KLKX4~qS5PB(ZGh`q zB={zQBLeRhcvRpq(K#VF6^Mlb@lfzu!50bMT5u=bQm{$zsPKCLe^4+YoKfN2E%;G^ zCj`DOI#ei@3wcy)g)*vzQ(Gt=3f?OCCc%4v|Da%0ICl%@sK8?=yR+bg;3q};x@c06 z?bisb4YJN6!CM7x5*QUYB5+jTks#N84Enbed>{A^3Qh`N3$e9Yfvp0gA=Vrfcp`Ka za0)Z55x7X;CV?XY?-qDe;0b|LB>Dmu3EU)bMBv>5j|w~?kcwG`e-_d0H3N_-C}=XpQ<>{buJ6 zoi957*4gel4n`S}~bg_VqdasuPKCcFUnVAan6XI8(=c=>U_D{EP&u9|f^ zCNTbf72{tP{WnDay$aU9r-Jp3>em2!EB=Y`z^?&csQMk?uWSAQxVM<`v#Z_(-0f$4 zMKSY3llTjQ7u51s>541z0Jj(gfU^ZZx19O!S26#~6Bs^N$7rw&zR4|E^F2bpAo?x4D~vpDFMS_wW3wm-vLng|{33bo7!J(2HKu(OZ7NJp87B zjvfmF2GL_Wp4Te|EX6+^eOeA!j(*kg>0%XN1%9_i$NhILU=>XUtj6~(bzo;;R7?TH zIS3}-u8NeF=aax5_1`R(f+yZaDV+&? z8K6$Nv4)2fhPP$8&D0f%gIGl)&MuMkXMBcniPipy7<619%eO|I@GwS_gapzG!$3 zWjFNEPfavB0I1Ul_Es9sOS*wy zNipD80qS@z2cKWiHGn!@3x72{Nf`%z9sJes#LW)i*TY{8-~Q|aehB{R^dLOd=&OJ_ zeGQ&!^mRZTd)h(Z-vHF{JkD;w@4!bL^L`)TWAIC-C*V;5t<|Ohc53xF54s3=A#DIY zA8j^qf^j|FM$ggD=oNaM{zxC-2_&cH)+)5s+IlUi4Ql(eUueJAbiGQSs?X7v>Ff2~ z`j_>y95*@ccRb?wvBT&5lGE)fcAepBc3tGU+x0cqwq9`@ej{kHeG_jT{TcuRfLe67Cke1|X!oFjaCmHql9&M|Ss zoPNc8?_xHL<*krsvvT#`LdmgucgYELuHL;hb(Tdma|WD@h~}_GNv_B-bW?;@Gj_Uq`Z$jrCi=e-!I|vKKe4AspIREQXCE9dTYtO z^n`Yd;`(3H-TDvdxc)r-v;HFRpW-RTpVKzS%XE$7ujw<6AL1#+H|c!mo3z_Cq&;8^ zY2PrOr+eJ@YEQVor+GckYY%#Ut%beMYe(_^SMSRd_Ab;H`cTb+bLMw-&1>X{H6s zV*_pR=+=ap8i;qN&e@z6bi@)pt*O0&@ zshDhF<{TDxCZpXu)|h5*N31v2JrFk&tfge4H8)3VZ7em|J0L0|V3pZ|jrU&h#mKz0 z5!9DR^_$6orOBk36hk6pHI75e>EZx^t#djRr2*fed)jtcc4p4a(aLETs|+Gkv?`w1 z5$jnMPYqmvAKl;7LKh77_C`1NGP-hUVz4ikw5ip}o>(&0lfztMCbq=44uahoP3>si z9gPoI>P*9ibSmTRv4L%7&*H&&FRZZfC1ziLG^y~VI|rk^@qxWrytF+_L%A|wa@ma2 zY-k|Xmle60rwiRoTA3J_1+f+5(%pMw9ysSr3?>y$zHI60 zYE77ly?y3js&j9Dta;O^xHG|BHkjzf<^0&*i=w@Qu{F_n5+owpJ(x_!5|}A6Sfk0k zU~?44fS-)*9E_zTlo|b&Gf!s|0?X#bX(?}&fXmA z>gpJXA}6&blhM5^6Y&A=x{ml2v4tpP)pfe!;18HPVhOH|wa1QtmQ5KOEpt!j0|Zt* zr&9qx8AD=8q+(nZ7ns}CwL2N@pG#X~16}Jnm(5EX0X9BeA^Wh#SO;fql5**?^G{P2 z{1l=zm|B8V!7hN&ZuSiJ#?GMyYm)I@(Sg{?J`9gOj66isOti%YqVe9;IpSZZIS}n_ zU#xJ>$w(R&D+Tvus%2Q*qNP|S`d~CiZyb{~m#8UYS#OW-0meoZcIKJQt!*>UoVR#s zbMx#ub7w7?Gk5Xg<};Tqn>DLt=A30qm!7#~&ayepOBT2vIk|rIT(t2c0C6wkQW8Ca@1Kkp>rF;63YEn$bGN?o-n#4R$4T#2GR^V1I z9mi$eRWLbLb1ZKlmWU<>R`yW0qz@qMJuQz7tWLJ}^iXGVFJF*JV@doe5p_PfMR_|8+KqxE1x}Rs8SObQg?;L{c&<#Hn$PtwM#y&bn7dC=taAXH z-)=L}lNxW*8jALaA6KOsz^4^U3O}vXnhxWP^3u9^FY?iNf5hNN$#^StigHVLsr}L^ zM934eY_32qizQE?urg)M z#P^K1yCmAZE!NXIa7rn!HIh?h^Q3;JK(c-rvySh1G)EV6o%f64Hm2Gj1 zg%sbU@K*a&H!rjn9fUPrQrL=Uq6ezH+N?>M{TD?0VuVU#oluYfu>Y|hm`bR7%T@6$ zF)nTI4J>1{QdW14L#!RH#S_qo4dl=qnY56@cYz75RJYl$LeG<2>J)?(+HnsO6-w>R zSz%>rWdfGao2$2|67YTX=IwA#NOY(wkF~?XV96TLBI#CxTksCtab-nHx(qjh@kF$D zCGO*KpAk*Puu15R@l^#^d21yLuo8gSBA|qt2q zurf(AM%Ko*bYO!uuy-xC;;9T@Zq=5m?KltIED>+L;%c&^@1lFub|X{Nb{b>8XRv!9 zBe0_@<7H2lz%C&hz{K8632Oofe;~d&&iDBlDUV_qyXh&WsPxEIIa@JV8OcZy*otB( zD|{E46cn{8%aG_JI) z8#{}0o(VSJEVK>|KnFMMZP?cI^7?0^mDvKdSQ|PSBn6YDeL2RIjTT81P{IacRO?M&bD`T zb*_mfvv1DQEM5xid$Tmt-fN~A={vJDCv#(#W?382G@X50mS&EJ}Ou zx+{*o?`h6svD^-Hb!}FMv($yuA(_zHIm%WHA;5r3X0z1ZD2Y{gOB6@uawyPmVpWOt zs8MGtbnr>RK#Ipinw2#yVbxyQvd)pgMe>yc1fb}3K%V+Yn$Wg(Td6g%B=Y$#$awms zQ+rZP`P|M(Zv?TYQHfoSx0!0{R!}bOCXyAH)6-O_QV+`>oYr>K2zO312ZM2X`aNY-9?ijUm!Xb&2v}UEk)9Sl(JSeCGWD%`!hx{#4Qol zoeW#HFzJruEQ>?eKCD2ytWL;EYSgtOwwKdNT0gzZY2w{Lie$mJI!|?&eHukwu`vv| zkrt*~$NK@*yV5#E^1cvjx3Zg)HVJzZ?9=R{Efqcc$7k-5TAK-Y#S<;8xS^Qv*>=pCKRwX+dscSuIODqACfjqaWMZ zG)t{<_VU+c9XCizvvK<-*XHp68Ou|h#m<#{pPCJCto++(cUtHj-Z8u(;T39d&JEJr>)G zoe=gqM{4P^_8)6H)IBVwB5&9z^8uXH)Y{u??&fZ`dl)IJ8y}SnBysz~`@-e10ecLg zgJ1<*-t!Fd(v-fXU%h1uS|O+LnKen)Ad;@e-SyHvSc&-d7mEZf-2=cau+~7%j51AE zCv6U5sy=>37h{-OM;d}=&MVYT$P!40KH-OJamSETdDnS=M1|G*0lWmEVt<#f6RsUOjrKa{DY1JH(Ke|B- z2Q2BzM0Zj?pORj)@~}NHvXTov)3HB?Rhnw9$yl@IU3zORvrzigmhgy>b3VddPoKh= zmvuU*Ms4P_kh-E-1S|70r%nq+H;e2<+Lzl%!X3z}XbMBwc_|HYUnY-{llvh<5(ZD8&=0ZD3Wbpouvl871sXMY4-t*Lrc8D~+>19pj)XxI6{OPYw!scO62@Nyz@Z`j zF|?~>gIwh@R1}AuK|KDJEuU%0JHd~N?S9mO9WfrfOX45n8EdoB+9n>UTLEbbni=_Q zOOBs>fVBbCmcnCvCPI*1=b6y58riDaXj`(qw%fH9<#xgfm(H#yD@>zdWnw9uAAqy* zoT_X?E5ILXfWy)b{@9S&A>qJhm$4hqeY^|J-#Ec4Ta?#CbxR`im@`u@&`3 z(XMSD-I~1RX+4_OggN$Sm72w#Z$*p}r?k$Kk9$2Ku~ZSJJnjiqhFbnB<#}?@IR`tGC&<5@nSRRB9`s$2m zy2g)e@8mx5MrD8Qg-nm8J*ndw?R3oko>HsTjyxy+Zur4DZ~)H*^NhD+_>sQM=P|9c zQxdDK(pUD#`iPt{Ert3vBbJZmjRCTR!g@BKjgsP_YH^98Y^0=6cjZT!Rs6 zif#=o@eHq#Uhe@^UaGR$TZ*%wL$#`})M&}{K&CZBSNznNaqCl$o;Uh_*_3PU`mvLo zHJave)R5)^5)3jK5Og79@av97SIqWG8dGs)aHHQ%;IR5dM-6%!l=`BkT;jP=$`S z18*nZt`Jw3t6~|~t*gVjI<2cK;LfqAhvAb$$6qN3yU?0K@kLsp(4yi{5uD7p5XRrw zLDdG{?h>wkyx6MBu&(Zqdz=M5Z0KRPj=w)#9J){sR0TMo!BKomTnmnz^b0j|l7p{H zs7LFZ1_VyG7F>X5$AcqB+|3$(uCy9|wgD_>4Tbvh-Ht$@I5_ACZ43lB)`8$?i^pbo zYaHkdZimyY2XwS`WYiJx`CK)+R#|2ghoX7_Kb7u)2DC&f>YCE@_7%&qISp0Apao*+pCcCBx)LEm!LhU5Baz6AE=517*He z4e`j+>T)2>^giIOFo1#G`g_1P=^Jp zR=aXlE7+}CwH7MGUj<3qX1_dMA3`8rholyZ;NcVSsdlh09L1b=6r|gsRQ-jRQR(J- zU{x$+wGrXAT?U#X2;@*cyOkbaz&DL2g$yj;cvz(>QG$XR109hW-Az1u(cC|?I7XQi z_aa7tYQ%GCGdJSDVuHt?$K%6DN4(o%%^*F13E^`)Lv6m`$jhNNCw}=n;KE;vEA(Ny zV4Q~9Jd#QZ!M@`-)F5-_YH%*Iytu5O2I(ryR<4%3f%&R?xbDlK?#buefbQd$ zCtTyn2{fmX0LLn$f`+4ozI-%%RV5F(H<5%%^Avil`c^?ht1Q&>r0Nv0rLNmlU@l2TVR{08=Why%Bz8! zVw&f*Ua7+kIORdW{eE6G{Dp3JrCt>p`2*_wLnwx&g6Gy{Mh!6&X+9JS;Ja`-i;&Jj zBfk!W-9~8SUD$nJs|=0u+rBCO-^p@`cx)&zF| zJaNIg#HbPqY#r-~%i?(1kC9*FKa?vgH@J(dtd3r#CQGQ!BrIs0C%hnjsZ zAHyIrhD0fyHIPz6&9INC@KvQHw3SnlB0WBbTk~L8VJ_Sr9JwVl@)>jj5FL>Q%h#;y zypj}FYi^&;ItygY8Trh}r`&a(P+4fBxF1l(QN_KOp}f@a{E$~@ zHmaTtaM;*46>#OH;$4^KvBO-jFif00c%@F&?xg z1{#S10S|tMpX(nDj$VyniXYbDAG;rU0CpT2BJhB$=m-PXq2TBt9-cg{M1g1E$V<`% zFCmf0Vmk6DVh1kz6HDvJqnOqbBYa7ehmHi1-Qnor3&3)PTE46+66)Y0wm5nVmxK-U z*-I`#usU~1TdE4AgdFp3onhFJ3rQZ*l; zFQWi0SRsDPss_zbKUixl7AGA|qKySmcLF#VI2pJY7!2IV6%*HK9(Cc}DkwE)WMi8z z3~S<}gF_n`^yL$Bfbun*xc3|tHh25v_LZH^HBI4uRd5v(-Q zO`v=Z40=eP89Z;5W++}+Z% zxo7U&S=}?wY?;}-xqEKQ%z4c%{Cmt!9_BMOYGnIOxt~zgpZ@!Mgg;j^<&;IzUpBJ7 zK1-NLhyfy@UT|JQ0vgU^i17pi zi(KZzI@pkECgHaUYI4o|Kk6@z0RO!a`TIK~xu#?=;L|J`$7j72cyBn0zvr$79J7GW z2E2$m@ZGwLup+Mowi2gy7l2*~xC}VIo^<~90~xP8+~lRM^26Mqc|fZvucQpy@YZ0J z&(N0P42w?~`7;_hor7$Ol*)X-#pl5G=^>XV`nq!fMm6}PK0(66nWwO{Ms%9dZuM_A z&KwZ~{5GWzW%*o=KRw}d%e`okbtVY;5_q#lPASz{UO#vXq>Sw?pCPAk_Qof-{W-p! zs%$foBma_yYUxEN%V&q`G!&+(1t+D=={MIEfIde=ox}6FYHyZ@Y*ob7}QBO0`k{+6cu`wV0)(zMKx*K#9y}20})0^?0i+{5~&%~!I zEr87;oh$e}P^>3Ev5#|F?vGaV0iWRZL9Z7Ps=$KkV!C{*U_i L|J?dtjllm06&OgG literal 41984 zcmeIbd3+q@kvCr5Gt)EE(v0TNY2CJE#@2!43mc5FZ21BkACir^B9Eo9En8B{Gm;Nv z%Mu102xlPNB$$u|maqv4*EV<A}5 zz;aVJnr0deXUED}P_!5VE%@DRD2Y`HpYt1{+o}ROt$13=wC~pR?a%*I&D3@4K6P1E zcB>i!8*Fwaw^hUJX>5a+Ho*qCDqNU^4Fjb&1z}QMu`1q-P~8sryZlB5jqWLh^?C0V zGj7#F#Y1%^s*-sTt!{^ho-8WnW`_{chTGoDH7i zT)Jl;XVQc2eVj=TdiHT9J!tHcOybu^P$!r8)qoA~>v?y9W6Zq;&~^{2G{hd|-CeeK zxeQ~Np?J5UGd7jg4|Ac0KnspD8wmSj^;(~@Fi4bq$j&KPR7WU(`LTGO-G88fXJS?r9J z)=Xwk<38d>*KkW8X-6NaNcR!8?62t~Z0dj1N9szIlk+2b-Htr&OAMU8#JJt(%5-se zAmXaqfx%vWBLhc{aa(SJF7A)IBksB#0TyK72r_O9mg~|UQBTBEw*%e3{6+?jAmgX> z;#sH;_vYDr&5Unmqn8RdGrlnjFun_*<&^Qw;>Iw;-0{u%4bh!Fz6+l`wlJqxNXGHw zn{!*zE5?oQIa>Dk=KOz6+p)s`sO{3@`y2?fXXe<~x%Ty3`+A;zoo8Rqx3Ba0%0APn zjRyf<>A}xB;Z|0O!Ot3b@Uuo9{H&1&KWo&3-hHf55Bm0rMvk#>Ko_C_+5nTF#DW)@ zGxvEwI~H&W#xO%^iMfBvNh(}&sfJWX8bd`8HPF}+W0;|e2)pcvz?K=q3{^zvv?Bsr zY78?}5n-tv5!iBLn4v@jM34)B>%&|VL!h+)C?ZI!@VREjFhdKInH@n?fBPy7=T}YR zmk-^U(_5r+<9iENPxTgCI#adN#1N^|_!uHp8XrTXMiN78-I;1xi%_l;wlcM{(Dvzh zX+2_&S7Odp=(knrn8P*sYx*r$?LX?b9CNq<|C*S?mH%sE4twLTiaG3`|1jp%Rj7^- zH6ljcjwpKYjSQUN#JC;BDs|}-QE$Xsx1*2+890KBpAyB8*F9|_)+K^q&_YHr1OcPS zVF&_7vBMAqj1q?-$WDD^F)Fo>#r4z@MwzchGKROzbL*vt5~Y?Ry{^bw#+T}QM*$|(GxGCKZ~^(fA% zN4cs-`RPmGxP|J<>#y~bt-sh3Kb=7_G_vk>|4H4aZ-f8;%lIE@gVHm%!T-LWbi#s* z_etRh#oEz>_YGpiM5t(AO0Ihq8AC9vkC0F?ScQ(x?YJHbcR%v#DfD>Lx&**v)vJQ-X$8lz=N6Hp8=*X!u z_5I%194uTKq9a(ExRZBfve&nJlTT#yM@Ony;GePtrhAjGe4s#h1d9WA@{gH3K0B5s zVVcQ(%rugIBV7r1NnZUGMzSTBbD@!3m&=)BB!_c3Q;g(Sayf-Y^7pwMy*SS+<$LII zl+Uk`S{SI8Y#RZ?UDsGLWduFe{ho0k6P#MzCFTyC4iITlkKQFsCSC z9!nm&Nfkq`hOb-`}WG!vu{|gSbgtPh-cq^g&6xr z6yn{tTacoCJLS4psf-~|ar|N`)@7G(((_>ROM*(_ds^{(nl$qWtjTV+d$_jU$W}MX zDKQtcjcm%5xi=%jDI+K)Tg82xyjfImO3ckFCbA&(W((e*(ZeaKtVFKN0~r}k8G+tx z6(7%(#VM++Ni4V}SMU=VJ)EM-+L|l#U`B>hMtX9S4`s^Y6jjz_7TlIA_)taL^F z=gNFCBf}{py}8K`XUgIfRThu5k-l8PPh|u-CFWsg_vgy|O{OePiNSg^W`rq&X#q2v$F`BUk1zmO(0HIAsKp zF1r=~mXncyWKM};P}Qe^nJEk#Glhwn7pFtG#4v{Hr~OX4u7xgcO2<+dH@m{PQ5425 zQy80WVZ_+u5gk`vB_$~gL36YQ$>Nw4Mxr@dn@e)<%}V8xvPdp2i{vs>7{BHym&tqd zv-ryEe?h>AISfI-nCdVDxzJwSbE>_hk2bpro~8g|NV3*T0}*Qz-R2&1dUaxC3 zrpTtiLJTo2#0_&zgi0jQb^63oNpv@QlXcKo?@7+Z1)0i(?>T*@36dB@`<<@5@>meY z#1NxQoM*Y|A7L{%VLgWUWj2)VZ$zD9qrj)|a@(jY=B>3cZDbnPHA2=Hk{Zt_7P}hB z*h9t+;A&V;B5ScLb`6Mam*#n=x7cc2*0o?w+25Ql>`dkEZ%B(MdrHKfI>Y)5bn`aZ z0M6xDO8`(O54EFC1?f6923%6NMvuFATeD`@B=rT(>9Rh{7q|5}zKHQWEwJ&{Q(#%w z0o&h{HbJd5(VzG>g5xtsC7i>xYRKDvb|ynP)~2XFh4C-?v2hmtW^rDzE}qF3r&tG( zH|nlm9ClkDX4K<#P1ldFG$NkZ^`NDWzHOIIpDg0w-T-dIP-PZ)OE%^`6uPaCO64fK&)0A+n~Gm!U;r{R&* zP0&*}$|)AO=KdC7s=v4_aCzU!b;cm0NRE$0C46)%gc|Y<>o#1c9CuAQPRK%R7f~ts zYsqyENS?p9Fbgl`pO12B(i^bOF;#yNOqxe9@w;HnY?WYQA7fG{3(Foeh1E-L)GFYp zg|&~a?*E`nDXDl{3@aD(Qyvs)g3C)2Tx6QyQqu$%>=4sNRQ$OM+D3UKDH2N;bCmru z?P7g&I9;T;n>LCm*F0|e8VqmjL7>O>p9=}QDGlo|l&&e6 zW;7T&Z?la3=Vek0*AzQ#rla-FzLb~6-0p5F(TECj?R5>!Lxbjd8(qcqhSxfTvP_o| zi$iI?Z-lGef*~Z8vK~TW!)2<(`n6J}sHZq;ggp^M)rzZOeG+1g`tdfYiBQ(VU`5^N z?gqNMN7X3oR;HtTyJGw2Fv{J*w?T?-zR%^0eG2pl`#knHz*1j<}S%@Zs`ctr`h)G_O@feRBBGQpH7$;sff$xN+L!X5l#7> z+sAqWI+V}J8k-YY1hw#)q~8^#cx-Nq(j@d z^bo;+H^MR_oVWq=g=s4$nkETOL6W6&(d!z0!w#1m}t-l91>Ir)aV_yW;*b(-OUI0~LPx2;C zMm#o07cj$}{A60*2pfelj9#;$DQqO)O$)ZNppo?WoSX~6s5kv38v=gc%{|kvl~`Od z%Eo24r|PqWh<@XfzMsM{bgs&y9k4ZN>E=NBYfMm*hX|xS9!}-`VdrUrqM8v zLccZh4C_m{Tq(O^~%P-b^n?F|+(^pHEF(KR90K8-mFidn0QC6R}34 zcw5wSe7d(VhOSb7^ETJE%anp`mxDh+SBeW`VMztcJ4JF(*n_~O#^4m1x{5;;zw+p| z`^n37;}~`$IHg>l2DfWMi>^-_VXxaxEfPYvj=sShEt zl{YTVt$T60?yiyzncCZx{?QpKJx#aA>ROsOsBA9BItTrn%3x>eC~PrD7KoAm0K9R+ z2)A}&Oo+e;x3nS}bS&jqP`93iptKMoGq_&6b=qtTMT1|Y>g4F$BZubF9OXvXElO2- zx+>2>rSu}ZK2N3VgHFt0S*=10yA24Zujk(4^r;oJ`hHb6Uc>QzcmnDsu~|)d*7ID= z;%#}UuLH?9qTb@DZ_W#FLf9MjeG(HxdDI{A$1wcN>b<4K7S^_A)C`+9_mnhylVNoH zdUx0?xi^|u9L&BKseCg4+ms|%1saF%WM@5dc(mDdA@L7q`>+XGB=i` zZbs|Zh4Xl!XhaK;6fUs-2}$9CI$qHk(NH*4vVm802D7k|GoZt|!KOdL*Zd9a7bEN| z*%0-GePQp-Jr_)U|NZxi1I4&~;(4zKake~SsHocCU?D? z$F`G#v1ddhST@38X2axgBpMBSqJ@z{Ves4>3`aq|h~(I}S;imngd?$+fMV{=%4M7> z*LPGd!_JjcpVg>O3JYPZ)^F~ywlLzidia91>2|()ozR+&;Z`5>W0wM27NZi<$BXUY zw5&L9rG$}=^}I8MKsw&@h7)=_0j)ifln(#aFw^L$`{Zg^`}sOD7bAvuK1Q9-dKtAG z`4||oGQI=IdjP9&w7D3#JMFFe>NI`eU+ESwnaphMH{$1N-!B?X0@5q8L z^(qvM&P4vyYmCie?4KD!`@2&Z4dy7%$_nE-h4-$D`O14&LBLqzFa!Z(slyNi3ZtNleSF9Lt+HBpXN|`cWt_1rM+gW<#iEp`l* zo0fyTF^-d#`EMrWW?yTUjteKGjyvW4kP-A@?$4NesT^SlLd@o!^9e`pq%FsL!DX5} zFBjH_Q}BXm%)J^JM=-;>8)lDS0(0Y>8NJ)x5R)4;4YhLO-!MT=_i@Ny#cBC+EZ^XH z;}@K{1}y7GD5Kc=F(98fyooFBa>ssxYvToaah)kQj&$$7AqeYx2LU6R9z(hHZ(!*} z`Y3jJihc%jBWaomOpFxip*|<8^#7)Qs^MeY;DBFz1DKIuW*1{MZ;f_sbtb(x_ zj8!r=lQFJBY!+kHz>bHE;erkmisW8k>TS^b(B<4D)w>L?Q`_&~AR(X+BjmdqwyLZ3@8Cs^`gxf0{tu&AP(;i~ zUhEwZVQlWgc~PG}%-XEqG7BpY>vupJO~2s32adddVAhw8N6^jz%-qH2W2C6Xk3PH? zndcUuZS>)NK-J+Clrn)cz7j;3Xnr`q`n*!>k5E%rd~Y-m`xCHeu!J9=thfHmL_vKV zYmL7!iZw>^5@aj4-UHdNJRFMV2_yDCCr7X>C zfM_@zj=2~K9?ze0+>ZjVLNOcfR6RCafJIz^QxA56s!cc;4y5p8oN0LgZ1)&UdjXO? zFwOD-n{wKuaK3nOir;bm?ri7hMNH-VJSNZAmr^KSE}=)TlH@|>f#Q?3DHu0FJIp)T7+IFAx(x{X)MetOZ0HbzH5L{*51DaO%UH5UtYp^7XwUtYfljQ57p? zm1ezHnw{78(FtlimvI6fVHEo&fUwG$Q&T@FTw_%*S{p8p)W#4OO8deO#~I0Waq%-Qy-ZWn+zU?kJN=*GgZf63cyua zl~S9%;uw!y>ns3!_ZP#17r^a%3gHRXRM63iy$axWQB_zTrX=1GoUdh*Ghhd5Og^|Gic?4H6rTqf z^ZA<1=K#fgiPkqFjvb3m=J`}ckxU-%6%}*mDwOE~7uKNf<2wINo<+w(0bzb>M%mYz^s|(j4TZ8KtA=i&<9SCR6 zXFeWtA^3+%Ae^md2sG+Lf5bJ4tE1ApU5o{T#tPTMa9l$bi?iCVa>=%T8ZB(UaG{3DmtXmK z@0{7x+%%(kMhjLigwI>_0=|R3F$ojP#(JW+CIN3BO!o9|NwLHVY@B?4@Yc0c2PW)^M~ewW~i7=zb*M1Ou6^g?*o1cNq47~USbqpX0Q z!qUaSDaVqiK}#d+iVV6c%i9H=npNzwlY7xNQ3 zmr|@V|2M)}D)3^_`GDk|U%)ai2X{@li~hZo>+*6D=Uq{{YeGA1DPzw0(SMj|P)g6o zS>j;uvEm?|sAaejxIur`c1`%2o%eN(t^Ex08q^@Ae=kr1&m4oz2Hk=BM(Cj;#_uoe zEHr5hI!*eDhO|hP zcx)ES7?~}jR1tgZBKXsw z{K5@@Ps73>eJ;Qr+fem-Wsv3qqK8N+sVbJ-G>J9*9JLG5cg44hMe=&Fye!C;*9qtA zMXcw}$m`sVDG%|6do6eE5S1fh%}rC4h*NVBWf7Z@)*@+szGDmOtCE(7eUt|3{px== z<|2+xqA^hYwM>;#lcbq7Or3>KGI83+gG^1sXQ`OFRjBE-7G;L$kupwOOq+$eUUYWQ zwQL@ta;kl*9g{njCZlay^gisG7a?GWV z&}Xvp-AsQkX^%0#eOz-N3*0C)Ns_xlBy&WwFEUv5u94=?r zDe%rJ#-|AXkqKu(|4EZexh#)MTpu_asmsf!18%N351ReeX9GT1&hTHV82+Jx;X?x7 z6HdM0YYHFMTIh*j8}bg6t^|Bz;yS=TMK?h{Br-J4F5{ zM~Zs@{~Toa;Yd&3n$cGyKERJheb`0>^BMj+kKs!K8_a&-`2sH%_<7;~TmISLHwc`T z$1-;b{(!jy_z{7X0oHRz-Y{Sx{Gidf!s!&rVU#sPd(yZDseWxC&8Am9AI>YLU34Sh zr5eKr1m7?C3H?rRek7dP^dRtaG=^W*KLhx*i}ADF3=g=O^J~E`@i6Cp4|D!Sa82?? zHRjwQd9RYZdG4=*)9L;$;Ah+?0cUvL#?bBy`2f30--n$KY3}?C#}E8K5vM*G-K}AM zK<_eWdsPJdt&zPs%Q9bz=0StY#qj&C30R}8aZd*PlzS>**wX~K$1?+vOM%B+ za{xaqoIiQklfQ5+%+KyowKRl&#eTamzZm2CVx%t0+k{@%1`pTLn7IL*1R%>?37I1l z&tF}`(ds(i9ln*=mT$1BwZXaAUvE@YX|ilCw%ogg`Z^t~IA*x$d7JumpwHu`KikyC z=t0cg+iD4~>wMnImAZ$LHq}=?*W;n9Y-(-!j(iVYZBtiQ?F4m$O<`>h>Z3Mw*Mw0} z_bLju{K@5^?+B%A$)jr~a(Q1Twk4lx5OtVhTk@%SGE>+2x+kvG12o5`3aaON0<=I; zQojIg5K8GRpikKp>kQE`n_`_Ix{yO4biO=cr5>iWHg&vet|v^F+0+fFUzj%8)Jv#e zn6}x}Pb-E&C2i{FicwHk+0@%r*Mhp*rq);70P03X!Ojtsa7-v=XBqwD6t+dxeFEKq zMXpo#3G_Q`bJ)&zQHOGR&!)`aE&1i-ovP9vubS_vpaPpZTy2zmlff)LF&zJyq0VQ`3v@%&($zZR#(T59e3YLYoR#{T--_6oon%R71T&-9~rT zd?~+%zGGA06>28drG%O1DWUGLsUOdu?iw z_XyISwy6thp9fWegI3nkH}M(wB)Z?G78ZRse=@n7Robts|0RD4Jz!I9Wk1ZHN-qd? zC*2-+Gk+TW!KTjg{w{wy?QY?Gchb{k`GIq3IZjcT`bJp@)Q5$-t#r3gzZ2>@-`#=v zo^$Cvn|dg4Xa2e5#m11^@2#SPbRHGhlveye{&`etQ@@&c%sr24Z0h}qg@JiARZ-&W z^J$(?chb6U=Ad2wW(C~{J<(& zfa3;EyS939U=5ApkVtyLq#x$D6U|d;Ev2DA2i>hG-&?g8fqK}c-Y;JT>a#X=i_s41 zMVqRs;Ivn5>fJKVchaVgS8>{}Z0ZKl@<*GxzLL{C=S%6n`Vi}E6Y4hF>0?`F&R2H+ zFmeefH*a9jm-ZK58CXxBwyEaouD}NR@B&WbKG7ezoUT~Jl=92v^f{rf^PN@R=eeAo zv8g#YRJxp=wJ8_UuAqOisUoCZL9g1>8x#6GSJE+?dK)KFSJF>xDq*ZLK19E=sjG}s z;6wB~o4NpLo%EhftwmZVdE3~s>wFI&Z6g)f)FVjSNToLQZdsovPBk`VqP#dwwW;G( z$K0E!#io8%wL7qh&b6r<%8$9bXrWDgtbBK%i!QRM`p_z)o7!#aoKPyzO;^~|^_9om z3F@|~yDE1F64d9UdG`f2(~wPl!uw%RBR2H~q-~+=Z0egx+d{Y46iw{&Y^A$wDui>a zt@NNxU0i%epobo{sg1=S2lb?)WK?aV=j^n71qW#xybwQVLQEHQ}a;5 zcKWGJorEWP>ECSXZFr)Wes5D>tLXFe(fc;_ZJa0ek#DhTl^JN2eyUZJ*wRlkg}R-d zE_^J|PfKk|i#{C~pmjp2ewL(oR=y;w4KEFeu)my z^+?-IV>a~*{hNWS=%7u#1{mRQ;}nEw@mfO%=O;61bIWY-*ePw}IQJ!KR*Z zQ}Cm-%%IL`GzvQHy z5b6NFlcG3yFa2Dl`9>m-828a{Z0eint@qI%ZOZai1n(nl4cFm1-$tSQib8o4k=A0T zg?#fpAE!k^snS1AtJ0DN&Vw!y>HvL<>Vtebq|x0t?9pjGA0N_l=>tZM>an}liSK^m z*+oD%-G`$f57kuj?*2ZU`Q*y)!KzTFC{lCzt;nm>FOkeagMk@-LlZ}5vN?#y#s0vZ zG{>dg6&&ANxQR6v|^37jjiRp4@g zYXn{@Fb=3u51@hBC|OoS*9(3dU^(3*@MXXnIte%hr6tQ6s8%9vtwh+VI_GTx#QB7r zDCNtE(hf)(c-Zgd>MH>cR&2rdDKTClcp>(TyMQy?Av?xaeQNd1G*ix@X39C#OgV>| zDd$i#+PJ9Z`Rd`g}0nH)Wp?6O_NUiz}(O0ymkn2sYRXDK+R-frnAzUT~6SSG!*YG*GX>LayDc8ir=k!|qo7xxh2-!}@i#UvtmX zo{qfeep37P#D50-m$GB-qx$O;Pk{d|@aJjk3+DoMmxVoT+O4%K^%dGkWUgn0);9?! zFZ$t$^F4lj5@LOa*m;Haju>NcB*W6bK-VON? za1I6!A(gGYLf}zt@`M{Sw(Sd^UeWMPJzDmxXHf9H+KZ7lJbSfu6W{QBQ-7iQUp*&9 zTd#&s)*5@Y0}~D78d$heFVnw0vBW6T?<}e`dbPiJry7Ux$owJ6+hPn#y?UjNgV0Hk zVIS@lI4F>9TL?Z|epLT-V7YNr`$gF*<7NF1h24P9PS|GrT>D1B4&$42WYR9c6O;A> zz5x72)CG4+;I~7Drr%oo2sq?>9FVm=rax8wBqSdxIA+{0lCNt=CcSUGu6-%u_ug;Y zwsF!H@57S%Y4LWe{>AERy`L8UKP=vOSR|j)9*@lTJf*!+&9(k;;oaV+kn3^OeGl-5 zwOJJpdA|$}_pvJ{e#P4>I`;}36!@ILpX={Nj(LBsA1wQ&_c!{d3jYk8d?DWpQu+&0 z?yHcT>pmu&Kk9t0bWHe%X}$NT_BuUUy~sCJ^OarbYth=UifYvweXD`j)}SA1hl{#= zty;a{yuN&kIQ1#ISzky`(V%MypvV0fX)Yd>ag^otzX^D?`%QSV)c+QGV1xf1ZAA0!#mF;~`ZfO{ zSaqBBnATtL4dCA`e9^z$b(Z%Pf4=@`^)LN}y07ege}&E-sL(k|?9ko~RGT|&Z7tq9 zbFxS_h~!L>oGFqV@ve67K#8l&u&cXlojFe|Z_|0iwdp+iK22QT9a=0n-*X!*+3UVd zn^1@uP224Iuz8y{rSu@JK$Ljgh@&p|o2{0$5Mcf+m_l69LboM!-dMF5pGN zZwG7@ZHuUj`U@6|&W*S~)L#%6ewXk^g)=6c1H!omlCwh(3jdJsKPmhp!Z|9OXR*ti z9eP%Do)pfHh4W+KXd2hTrLl}lOWUvz_`FMfrkVh6?jshX0p87 zWJ_uVZxz@koVeg)0uKp1D)6L0ciw%pCRCfp8d~$X_2c0DuwYE!A%RB)o|IHApHpk{ zxewIlv-|?VTfzTDL0mXv0uKr2NIq*mDx4F7pA?*MvW9jEaIPA`YXhQ9@K(V$3LY1k zQNhQA|DfQ91V1A9QNf=Jd>%Sa2jit-{|Z_^7}K1s)N2 zLf}b}p#s)ZBe1qW>=e9J@Qs4U3+^LVXjJeq;Xf$&A;FIbenQ}z;LHwDNOTIU4RH^z z4RH@&Ae`0^m$FgtxZvG@hycPlB%C9HpAdKwd4E{&rpQoOY8{qZ3%)>LYnWv=3LY2O z4Vl@YQNhOq9ujy|@}7jeE2KqOTdlxWfo%~Pzk-he-=2R+U`_M@_XmL+1&#`QP~Z`P zCj?RfeQqV7I9=aBJe~JTS&#?Pk{>rZWK7G_$8dW zK;TA!qXKJ6S+jLvGjW!2+ zyz@}f-M$u_t*j5u2CT1Se6*bL?(*{h@2ldImzwIuj8C`_aL+`RiB_{rYdPb;tYZ8g zk$*+xe^J5mAFW{dcdAwaURKcoI4gK5;8&`y1bnkL4mi7*@u^h_z`qGF?kQ%zYmx=H zcw&n2id}%W8^eGT1rJVO{x7STe@{8X`zjfp7h;)tuy7)4=q|n)@a+P&ZGmv!kzC)c;M9YnGu!f+V*gho zcO&&hkxY5-2i`5P(064}wGyB8c<}b(k3U`O2lS(rbhK6gFc06EprggYfMK+wj+QF| zEWw|SHZ21zL%ZtuTwWz$Ilf;<$1c4Vu#zSLR^cm>n;F2{0ChYoGYj|`Akla>N*#*=FrzENN~@U3(a@E$-N_XSo0-wvqb`MXuX z`v7%3W4Q*n1&A*Q!#7H3IEiQnp2TMsHLQ%*0Uv}d8Vv#Jc=F~_;JW~I+6`OqoB*Is zSHTtytE4M|?}aTI##$%v5!{>6Xg?rMPqE(8aKh3B{Ax-7KLDuXiJ&dOuLIQSAne7H z27o$FVYUOm0Z_*?K7GJ%1k~~5js^G`SgO%y0d=fxQ@}q5h!a6rs$mB*1pF{8)#(}7 zsMD8VolakdT{?Xg7KLcNHWhG#R*!yg8SpS&0emj%Y~h^aBXl>tfZg~pdXwIz_wekK zTk~q=+A8f5EvXG@!`ctD|D)-8r9MTUtuN6p(Rb;O>*u;|aXsq#d)Iedru$>==iL)M z&7Osxi#$6#U-Ep*^Ut1;aj$XG_=7R%-Rr%@`*+?~z2&}1zG=R&f3km$zsG--|HuB{ z`Tb_IIomwn>@kPU+sy}`A@l?HD4%#`zdnHzJ2Q*%Me}T&qMgc{jg#wBd5_7tUvAzK zIrYrt-H#K-Q}ZssiR-Dn=f>$fV*UUp#r*vwUijDx-+D35{1|V3L`f4P&O}5Gun3~+ zclcvGxc)@+7<4zDqUZDQW_~Of*QyeiTt8jnma9?fk?S8s9+4Q&%JZ4|J0-aAIG6k; zy(0NdS|s`L?NgE;PZ^eQev`JA@XTP+$r2nV;@U3xD+u3BhF7(AmFTV3_K6>AZMqip zWAtV1dFs*si5}EnqT~9@^b7qp;NPQW*AHl`>kYcj^-uJG>m_>5^((r_U8CLSuEP6Q z^ij`#?aRh~?Rn#6`l9y{?IrJ%+E2Zo*M95$f|lp|BHk}*m;1h_z3=-ZnrYtB#9&)bd`rKT8tmyx&EJ$xXixNax2E>=cg^nx z50ZAKGv%=*T4r_vYwg-G)RRoCu)4OhO2uRa(`R#XM>5{EeYIuvwkLWMU4uPVKTD}J zkvb)7iPazqhA9(V9%g_srVZLMwXu zw){>?pGhQRKKr?$85 ziuVlK;!H!w+C;p2Re$fE_JMdmNa+Ebv@$Wc)#_e2)YA*4T-GA1Zy=sj_~IQy@!p=n zJz2c8dRszSHfV9)OdVL!V4}}<3OjfDnY=rlmiG_NK(gHoXA-m};9y|sX2@|c68f#R ziEi}JuEF`U(DRhX`LemQv$fyq-_vIer8@QuB$_v#jyoOPB}4sPxLlOjvp(KClvo|_ zNrHrfyM~fU^s2TV>Dlq*9*`b5}Axa1L!r40f*TSTZNA1lai4 zhVj7?ry4k8qvT8HoqLA7;HTiFq0}No4mJUdl~(sqZ(=^pTb=CL86Qk6??dt)NaKsRsm4f*))G{ot(PGRgeNdVsH;&1Yi`0;@wXclt z2F6MiHfvUMYuof$a~3XcZk{>&oEeK|pR;gb^Q9>-5ZAkuJxG)(VqJeKzB#d~pVN91sF$55L$lGBBzw?- z`n%D4IOD(o9Js_vq6aXoT(-I$k=W|p2}Y`OAeAP#dOU7>Fppe?#Cci7{GQIvB|V8= zaCyzJE*0OB$l~$HSA9B_#a-Nw4+JH$IZM!$5m>WX?L%E%iBu|wx3cFHMk~VjPFc!i zXSLn6HqmG8jAMvoY+T+CMo+g4plp76t&`2fsL5KHWT&(z5CAh;m-Xb3yt3jRmo2hZ zMd55#hc&PaE4yqK(^-0Rhwc3Bot+CYwDD0Y+vBGW*YOfEM#r-krANT{H03-V4yQ>w zwd;=;Ii=5!7g%Bq^>>eFXL`!`G@dZVbK&*17+jh;UhkrK*VaUL>)>fqd9IO|Dm_=G0EMmPo=nRI z2M5#wocd%PPRmog?q8L>WGiAW*6LjeS`_c^N?4(LJ(0b?q2+Bo=n^U3yzrX)bekGli*~|PFOh6nyuTZwJn5`X zS_2oy`x1mg6CDr`Kd}9YZm8;4JJJRtkeTG&?Ar z$X>kIf>f%@8c;33V_nJ=gauY&GZGg{Ez((Fd1`q-G@(&fZB{v8`>IXbV4jd@SCt=A zh>by$-LEC3OZ9QV+p+b^@{}|gHi$j_@!sXw{bR!sPbIKc=uPldIagV0$3QXI!Lfu` zm0UM~Ne((C&{JDt)6kYJiR8khwM&{sT!QshOJ}DzL{=6(y@{kOu#gjj0dw@GA?Q$< zxiK?5XJeb;j4L9O)QU;FZ&UA{j-J769pl<6lcOV^gy)ywSN9UTtmO7gf>X+}o^C8< zGJL0qC8)!C1R)Hw@rld(H_M6#iSgbuN;w@?#%e~%+QjB|tg{C9ti_rKe=>INp{1vO6mw>%1W&y&NkwbuN(MIjUvB z*uN)}V2>2x5B6;8;gf+(DtCA3jB&ArarN~`6WG|F;8%4?2>)k>nFHXH%(;Ub5)E`0k#*p+4sfipzpG5LK}Q z0-pBW5=`=#C(#oW8{0C6;K|xv@n@J$AI1-5Ht{+$zasTg5bsXC!B*m}WXF-!vn=PfT+% z+r%`>UKXe6>`h{tdFmc9oshdlOtZ6gh-u!b8^m;i><`nFEOOHnk8!m-OtaFP!!#$o zH%xP6Yv@o;6l?A8UW7$Kk``gm({i<;M}zL{!AkrL=fjxw2Rl1Asl#IGMAVU3N*xP_ zHg86*P3oeSwX#5jN6`P0_DWY+YQ>hs+F)}W$NzF-F<@b?OLVJF=m@m)F~(quwWV2E zy%buV`8Mm+8(c(lIo$w?P5{V}0O1$X_8Ks?I+4WWu@h-Ln$q(~QbmB=wn;BfvC>jA zg&f8+)zqb+Tvl2LbYQlooP(&9DW^YY-HJgd!@@V40eiDaEU|ML9Tx8`ctLA#746ZF zW4jzidMl|$YKBA@cLbLttv(0GT!MhOwS9YHm&3{llWJ#@u7fipN`qMg?7tqIvN%
TDYQ={$9;v21j1?|BG2Hk-FF-OkYwjLd zq;j^!B9zNvd$RY&%3FNm=_u|@*d?&26E>)m=fpH6wQ`P`R9ZTmh#4x9h;ke4?2{uX z;x<^>(~tEroaZcI(8z4xO6j8o%2Oj$eaM#zeS)VV#ehvQIMKE1GN;+02=WI16so@y}XFby~6PGP`| zbYi+)c(tKgS1PMWUe98(S9){eEMXmlWutR4r@ZIfFG<-=lSOaEL0KjuJKZ-kiQ65k zpLZ=Q5ukdo0Oc4oJu7~wW{vMouP?G@e7ow-p@)d3k3ispbV6?EStV-$cvV7N`v6wg zX_lH4o%yoKJ}r@&X5-H0+~M&6IhDsXIUUc*!|Cj641sKxJX)Ix41WM(ayrAV3@rvi(2W3J+DJEjp@yc>XPV9$~q8D*HRN;(|) zRNZVxbE2C%$1{v(&RNvb%9el*8-tZL)?ux%b|sQLqOh6jA0)XI%t5>tWS4!vk~(?$ zoWl{h*6zNZ{?)zl{333lvj|CDyxVfp5@}Sr zsoV)s24iVBh}%1h&}{)#LMQHHk7J6;w*HhO)15|D``dgardB9v(HxTRJ;8^Ac4~*c zrGcFk?izAlN!VQVDU5kp zr<|($W==h+GoD4TFwb}D)Kg@$$PUDR*}D?jI)8bk#I4-tsK*qEH?Qb)Nbk# zDE4Qc%&IEnaMWy*QYW{{$-MTm4{IfZEXEv0$JRvu!adIU09w+yZzZ>%;R1EtWOeTm zZFodL^*%;9hlf#rtW>rmxfgHTPlZAY1*FpAj>2|4M#mqhz;P%37&=9= zLN0O%3hIHLAv`jeoj%=8?*Kn8x(8535@jt0#0UQI&S;yFmbUO1;xeRSah;Vu(@t*( zH;(rZ(iTCgJEzQ9kg_Y;s_N)ya;$dhwHEnyKnq)(T~3yp#-d8aVi-RNV|#Lnauh8C z|5OF+mX+Y2>N3YA?D*_FPUX3cccOaSzTHyIQ=7$c!(u#pIRGhkIc^XG;&8^!n{g_; zgKb%Xv@SeFc?oppT07g8Xtg`RPH1wZGkVU+xOGMoBrTLSAXak2a_49Qrw14}YYOkp zsGlQAMch!Q=Agw+`l*@*M9UzwW;$jQGz>u}&b08iJ)?U$jv&(1e@oL@TTpHs_1gNu zwaHtWmZPan7-N4`t{H6k7WkFO`QWk6S|Jp_czlejXe&DLcn5(cKMw zyTo5y&LDpy@F`y9mfI{XnZWbNv+*3V(@Ny!K4;+zSwARGTDt6vEp~Pje9ZmiuWaE9 zxJ`Os#{gP~!w1`*lwQZ-;bEvixSu>s_e#< z`{NeWkK2yJYuYFHYMYDkY&F$&Nk1NfhRu?fM{y%4l~=XChOfR!AU%#2>gBdNE1oX# z1M54fPpnbVpIaf*qG?O&xI`x$v%RO4YIQu%fxioOa10#8v(!A|9Up$6Epxd~E9sQ@ zYKyd$)3ZJxW=u_?yiM>WM_vy2?8nnu>Dr5rPVh9qzTRTXcH?U_*!lsYi`ro^>){yL zgt*LYrM%Mz9@nD@&(d>r;V8vrsi@n8mTku~@|*DNyoyCsv>Mj%1jW6M<#_s_Oa6N$ zw)nU3*3dTb<#*ig^G7wSvH>yTRHyrxssy(tRtme)V=!m*HeXT1r$;HOxr`{`t(jiUVR?%pW0e{| zqE0sEt=Z7{7bU|h8USEGP>*VU{J2wOtS(qt5h}trF0%Tl9(6_W^LZ#j$J>Rs z8*fj9i^~Rc3MH%+h8}W|Qphpco>eca?sG`UkJy;oJhla-*@tu<4(UYc7qbJ$Azba;;z;XM0r z@zoI1Xh_9|Em2}a+q^~L(H9}XJ%BN=VQd>`K+Y%yP|CGBlCDS5!3iN_6>noPJcGPb-nq(g&Us#CZs z73`H-l@|qht)d~T zj&QeQGSHlaU=C$Ewe*=mb1DxC=~(7?Sg8t82?aHVw6`@rY6lXULgV__|5dfky_W_a}V$TBy+ z;6CWV4+e%zj3jj7$TFWqnNU=daq>EX3YuE?`FtMq$MpuAApn z73_Fe;(>PYO$7v4JEjcC^`#Yzrxp` z=z-^Xf;g(NhOYo-1}o6#g$>2y@$l$3VY_W(n;)SlGBy?&o2xoTkiE{fsrFJM+hYZ)_7B*D=rQr|7r5Ro-)ArA+#i0; zJlFTaaP<8b8{b&th6~8;1~8W5Xbj)t3KT1RcQ^C#15NL{cQf{{_ynZ8c}RYZqJ}>W zN%-bnd<&f6(+%CHS|4p^L@veqN{$?uT1<7tjAWQBqh=c$xzgdRW+{%l_7n;Np;R|2 zpef--g!`3l1pIKC3MZUR#;MB)Z-B@=B-tx78^bgq{U)nWLCK~+$>3#{e2uZgoT|nn*01}IyD zIwm574uddqFzsU63x)}Chhq$fUBnCYFm!iW!RBz$8-jRis#tvGj)$dLmmaAJ`b1@r zR}>hrTnf4ZI|4xF*+5l zVgyHUdje-P$ZKQ?)*N1aCP)>`eTX1f_%TF(LG)0=ic!->3wZo;49_H~xhE@wpi008 zWmv5sH1Q&UV^D<&1WYs1?2hpoA3$TEGjK6*Gw?7l7nvZjj{cPzp)cX*Df!G0p~{GBb$P52ecj$mQ>VOM^Q3rpC$EkmKqkpFRJyw%|8-@JBR5qb4EhTV}V+=$bRPWlneV^i7GDmN}aeT{Gs) znzLz6qB}8r_RLLP-Ltw{X5xFY-8=-QYt+d4n{w}n)t~tTkA&aOnta+U>Ca}_A5JC= zGI#;LP%juSP5}+$(f#-Z0>3RRr=xAQyykNgBZC@}1WWjMq3+?sadL91(8kCEI#?SK=NL_2HTFWFjF? zVsf7M>!2ev5#J$EldIzYQU7ZmP`~erFIpYVEke2q?yQaHv)nSgHypw5*r@KvEZ{Q% z*Hb$_sJI?;`C4Ghae{a;=;eS*fb;7s?q9zry_q|sywnvZd-CG$u0n#%z@-gu4O;m; zZwb!R__UMn7s*K>((r2mfSH?Zd?xLjWFi5t&$B59J*bg=K8f^2ux7QKn5uKd0r2KY9>-cf&rabSkWY~Za%?+Y z-ev?({w5Sv)Ah*9=a}kb7OJTQr?$=MHp_Dua<9)$<1@BM>77Xhi_)%Jp;Pu;M+`E0K4I<>1fFWD5e?fg2Wu$;bCeHXg))q zjX!SjE>N=oTR_RrBtU}OqZMtyr^$Vg>4k@C&?9qOCSwP;&;O;b&#K?0yZ_w(OYQrA KX8o^v;Qs>($pmiz From 7caadb22cd4a5525adf2959f158301836e8ba0ce Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Sat, 30 May 2026 14:42:49 +0800 Subject: [PATCH 078/202] fixes --- Shared/CIPPSharp/CIPPSharp.csproj | 2 ++ Shared/CIPPSharp/bin/CIPPSharp.dll | Bin 41472 -> 41472 bytes 2 files changed, 2 insertions(+) diff --git a/Shared/CIPPSharp/CIPPSharp.csproj b/Shared/CIPPSharp/CIPPSharp.csproj index 826586ef6d2c..9bd6e19ccacd 100644 --- a/Shared/CIPPSharp/CIPPSharp.csproj +++ b/Shared/CIPPSharp/CIPPSharp.csproj @@ -11,5 +11,7 @@ false false false + bin\ + false diff --git a/Shared/CIPPSharp/bin/CIPPSharp.dll b/Shared/CIPPSharp/bin/CIPPSharp.dll index 92d07b054ef993dece27dd1fc7244bf175c1ed68..f74704b1943c28aa883a83925eb5d7353c3e74b8 100644 GIT binary patch delta 1795 zcmXw)PiP}m9LIlenzY?*nwNIjT?#8?tWv~(O`EpCgJej(C=0s^77--Vq&ckOrN@Gk zAypQ;2g@kSHi9!&5%Fdg^dijABGQA1hrQ?_L-*j#gL?6xzV`tW=9BOH{C@M^8#2T6 zPo4g$^TF-Hr3aON4)0u3Umt$CrP`5FOU2D^MXp7z^HlVj`a*peDd$r0PESNd+0n-6 zhVw&B{W)5w*B+U@r5~3)Stu>Z>+)N)b@PcJD?cMG`O)wt`P}doLr>n6Te>A3ar6uH zw`JF4AK$_EF(GDv**BeMI>smXV|Cg=t}YOb64!gqQ(lfY~=Rn^X2O5~|0_US5!*GSmrbjL9+Q7;)@r%3*VkJ+0u_ z>0HdB$qOZq+@jj1-zRsRfpm3;o<2Ur4`3Xi5(X(bBj->}?3t5Y=)q0HGwqW@n7|B< zC+tRZvJ`AC6e2k`9LjgPifO`j!Cpqc5ZIrXK?=t(hf=gv1*_17P1uHgxL>pz{j4~Y zpY$nte-blkO*(lXQ~DGWPaVi}I-jytHN6*@pTe{qRHyA?F8MLo#CYU3xesHQz!YXM zhw1|JKo9ybgbB=GZWx&ByJ$}+T(p~T$sXLq_~Z~KFoQWP%napM?apuqn7pff7{UZ* zRCBT_+0JUo-V<(VDDMQi$-txF!_XFVLQYF|a~XL|&Z)|*^=g*s$S%1__Gax2ZL*K= zlS6V$PRK#ZAY(8l=VU3fKzS$!x=MD-cCjY84f`;LDa_~@lSMOwW{YH(+$4K?D0g)` zVBk~e8lLGsCd9<#6prCTdZfbC6?<>13imd~t*}M1M{bk5CZFjL6Jt`CRRa5;jTz*W zAJQswc7ZB%=WI`t?7=qlVIPJtnX}7hrvI)!HTg{Ecr|ZlbD;S+bbK}8@2j!x4vGht#xXRUSp-< zN7wKD8@+fmKY!c2UjF}lYIF6_y<%Q7?;br{d{maCI+}}bs!{plpPlPHS(Bcu%7!`C zr6D!*3h0`&BXu+C7`rVirn+s8nsLjfS~vBY`L3CI&2;t5v0@HCdis-Jocj$k{qO2< GdFnrkf_C8m delta 1795 zcmXw)L1-gY7{|XiP1?e0o8Bcks=&dNWdLDPQ|h?X3;B zoKZ#nHC$M&JTZG!KP7vzP*{>T#pk3hzZkwGUm3n;=*io1Ro5jD(JwIH zkZUIU_yFI*gqSX7-%O7503YM;;uB1YIhL-OIcCkwnzlYgouZVo8&4=ZG@E2cL_Sy+5MIEEYm<)4@5yxJp91iE$%W{rg z&c!U6yioASb-E4aeRAM*rKtmEI`|OZg@ZOKrbx&sIfH6q&s^+64{jQcv`-FU3{yCs zum{b^lCwFNizGJe%g?%usli6h-bN?awr>N91dd?_C2zY5mZ1x4umL-8KW`8EWxg-> z^a*)?5;JK{GI>+-`UEqWx+&{Ao3dRsz1KExxoKOJr|o7g`68@gJaU8Ffdd%B1g0>9 z>I~~Z5Be~KF-&1**f#e!V=u_f*h9Ev4{l<7atLFX!VKol_T_i&p5+NJ`91B!5XLa2 zn~_z)PL>PynQ#kz`LL~P6dny9hPI(&a#FB|OUYw$MptI7SF;5wumcA$fhjX%vS?P&9FgpjYh+LNz=zH5@7ITOgFn^5^3;DC CPI>wO From 11e613aab2d6886caa68e5ceb80f39e0a25293f0 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Sat, 30 May 2026 15:55:25 +0800 Subject: [PATCH 079/202] cleanup --- .../CIPPDBCache/Push-ExecCIPPDBCache.ps1 | 15 +---------- .../Activity Triggers/Tests/Push-CIPPTest.ps1 | 17 +----------- .../Public/Invoke-CIPPTestCollection.ps1 | 27 ++----------------- 3 files changed, 4 insertions(+), 55 deletions(-) diff --git a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/CIPPDBCache/Push-ExecCIPPDBCache.ps1 b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/CIPPDBCache/Push-ExecCIPPDBCache.ps1 index 522d40cecc12..31f46a51e535 100644 --- a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/CIPPDBCache/Push-ExecCIPPDBCache.ps1 +++ b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/CIPPDBCache/Push-ExecCIPPDBCache.ps1 @@ -44,20 +44,7 @@ function Push-ExecCIPPDBCache { # Build the full function name $FullFunctionName = "Set-CIPPDBCache$Name" - - # Cache the resolved command per process so back-to-back HTTP-driven refreshes - # don't repeat the module command-table walk. - if (-not $script:CIPPDBCacheFunctionLookup) { - $script:CIPPDBCacheFunctionLookup = [System.Collections.Generic.Dictionary[string, object]]::new([System.StringComparer]::OrdinalIgnoreCase) - Write-Information "[CacheInit] CIPPDBCacheFunctionLookup initialized in PID $PID" - } - if ($script:CIPPDBCacheFunctionLookup.ContainsKey($FullFunctionName)) { - Write-Information "[CacheHit] CIPPDBCacheFunctionLookup PID=$PID Key=$FullFunctionName Size=$($script:CIPPDBCacheFunctionLookup.Count)" - } else { - Write-Information "[CacheMiss] CIPPDBCacheFunctionLookup PID=$PID Key=$FullFunctionName Size=$($script:CIPPDBCacheFunctionLookup.Count) - resolving via Get-Command" - $script:CIPPDBCacheFunctionLookup[$FullFunctionName] = Get-Command -Name $FullFunctionName -ErrorAction SilentlyContinue - } - $Function = $script:CIPPDBCacheFunctionLookup[$FullFunctionName] + $Function = Get-Command -Name $FullFunctionName -ErrorAction SilentlyContinue if (-not $Function) { throw "Function $FullFunctionName does not exist" } diff --git a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Tests/Push-CIPPTest.ps1 b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Tests/Push-CIPPTest.ps1 index 302276834eb2..d7224755ad9a 100644 --- a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Tests/Push-CIPPTest.ps1 +++ b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Tests/Push-CIPPTest.ps1 @@ -12,14 +12,6 @@ function Push-CIPPTest { Write-Information "Running test $TestId for tenant $TenantFilter" - # Per-process cache of resolved test function commands so that a flat orchestrator - # firing thousands of activities doesn't repeat the module command-table walk - # for every task. - if (-not $script:CIPPTestFunctionLookup) { - $script:CIPPTestFunctionLookup = [System.Collections.Generic.Dictionary[string, object]]::new([System.StringComparer]::OrdinalIgnoreCase) - Write-Information "[CacheInit] CIPPTestFunctionLookup initialized in PID $PID" - } - try { if ($TestId -like 'CustomScript-*') { $ScriptGuid = $TestId -replace '^CustomScript-', '' @@ -30,14 +22,7 @@ function Push-CIPPTest { } $FunctionName = "Invoke-CippTest$TestId" - - if ($script:CIPPTestFunctionLookup.ContainsKey($FunctionName)) { - Write-Information "[CacheHit] CIPPTestFunctionLookup PID=$PID Key=$FunctionName Size=$($script:CIPPTestFunctionLookup.Count)" - } else { - Write-Information "[CacheMiss] CIPPTestFunctionLookup PID=$PID Key=$FunctionName Size=$($script:CIPPTestFunctionLookup.Count) - resolving via Get-Command" - $script:CIPPTestFunctionLookup[$FunctionName] = Get-Command $FunctionName -Module CIPPTests -ErrorAction SilentlyContinue - } - $TestCommand = $script:CIPPTestFunctionLookup[$FunctionName] + $TestCommand = Get-Command -Name $FunctionName -Module CIPPTests -ErrorAction SilentlyContinue if (-not $TestCommand) { Write-LogMessage -API 'Tests' -tenant $TenantFilter -message "Test function not found: $FunctionName" -sev Error return @{ testRun = $false } diff --git a/Modules/CIPPCore/Public/Invoke-CIPPTestCollection.ps1 b/Modules/CIPPCore/Public/Invoke-CIPPTestCollection.ps1 index 052242225f8d..e03b78f77a3f 100644 --- a/Modules/CIPPCore/Public/Invoke-CIPPTestCollection.ps1 +++ b/Modules/CIPPCore/Public/Invoke-CIPPTestCollection.ps1 @@ -53,21 +53,6 @@ function Invoke-CIPPTestCollection { GenericTests = 'Invoke-CippTestGenericTest*' } - # Process-scoped cache of Get-Command lookups so each (tenant × suite) invocation - # doesn't pay the module command-table walk again. The CIPPTests module is loaded - # once per worker process and its exported function set does not change at runtime. - if (-not $script:CIPPTestSuiteFunctionCache) { - $script:CIPPTestSuiteFunctionCache = [System.Collections.Generic.Dictionary[string, object]]::new([System.StringComparer]::OrdinalIgnoreCase) - Write-Information "[CacheInit] CIPPTestSuiteFunctionCache initialized in PID $PID" - } - if (-not $script:CIPPTestCustomFunctionResolved) { - Write-Information "[CacheMiss] CIPPTestCustomFunction PID=$PID - resolving Invoke-CippTestCustomScripts via Get-Command" - $script:CIPPTestCustomFunction = Get-Command -Name 'Invoke-CippTestCustomScripts' -ErrorAction SilentlyContinue - $script:CIPPTestCustomFunctionResolved = $true - } else { - Write-Information "[CacheHit] CIPPTestCustomFunction PID=$PID Resolved=$([bool]$script:CIPPTestCustomFunction)" - } - $SuiteStopwatch = [System.Diagnostics.Stopwatch]::StartNew() $SuccessCount = 0 $FailedCount = 0 @@ -77,7 +62,7 @@ function Invoke-CIPPTestCollection { # Custom suite: Invoke-CippTestCustomScripts now requires a ScriptGuid parameter. # Enumerate distinct enabled script guids from the DB and call once per guid. if ($SuiteName -eq 'Custom') { - if (-not $script:CIPPTestCustomFunction) { + if (-not (Get-Command -Name 'Invoke-CippTestCustomScripts' -ErrorAction SilentlyContinue)) { Write-Information 'Invoke-CippTestCustomScripts not found — skipping Custom suite' return @{ SuiteName = $SuiteName; TenantFilter = $TenantFilter; Success = 0; Failed = 0; Total = 0; TotalSeconds = 0; Timings = @(); Errors = @() } } @@ -177,16 +162,8 @@ function Invoke-CIPPTestCollection { } # Standard suites: discover functions by name pattern via Get-Command. - # Cache the function list per suite so repeated activity invocations in the same - # process don't pay the module command-table walk again (item 7). $Pattern = $SuitePatterns[$SuiteName] - if ($script:CIPPTestSuiteFunctionCache.ContainsKey($SuiteName)) { - Write-Information "[CacheHit] CIPPTestSuiteFunctionCache PID=$PID Suite=$SuiteName Size=$($script:CIPPTestSuiteFunctionCache.Count)" - } else { - Write-Information "[CacheMiss] CIPPTestSuiteFunctionCache PID=$PID Suite=$SuiteName Size=$($script:CIPPTestSuiteFunctionCache.Count) - resolving pattern '$Pattern' via Get-Command" - $script:CIPPTestSuiteFunctionCache[$SuiteName] = @(Get-Command -Name $Pattern -Module CIPPTests -ErrorAction SilentlyContinue) - } - $TestFunctions = $script:CIPPTestSuiteFunctionCache[$SuiteName] + $TestFunctions = @(Get-Command -Name $Pattern -Module CIPPTests -ErrorAction SilentlyContinue) if ($TestFunctions.Count -eq 0) { Write-Information "No test functions found for suite $SuiteName (pattern: $Pattern) — skipping" return @{ From dd8952e4e9b458a33ba232d09d5103df9267016f Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Sat, 30 May 2026 18:57:49 +0800 Subject: [PATCH 080/202] reduce memory --- .../Push-SchedulerCIPPNotifications.ps1 | 182 +++++++++--------- .../HTTP Functions/Invoke-ListLogs.ps1 | 111 ++++++----- 2 files changed, 150 insertions(+), 143 deletions(-) diff --git a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-SchedulerCIPPNotifications.ps1 b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-SchedulerCIPPNotifications.ps1 index 956e9b9965af..dd95f3bd9c55 100644 --- a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-SchedulerCIPPNotifications.ps1 +++ b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-SchedulerCIPPNotifications.ps1 @@ -17,84 +17,86 @@ function Push-SchedulerCIPPNotifications { $severity = $Config.Severity -split ',' if (!$severity) { - $severity = [System.Collections.ArrayList]@('Info', 'Error', 'Warning', 'Critical', 'Alert') + $severity = @('Info', 'Error', 'Warning', 'Critical', 'Alert') } Write-Information "Our Severity table is: $severity" - $Table = Get-CIPPTable - $PartitionKey = Get-Date -UFormat '%Y%m%d' - $Filter = "PartitionKey eq '{0}'" -f $PartitionKey - $Currentlog = Get-CIPPAzDataTableEntity @Table -Filter $Filter | Where-Object { - $_.API -in $Settings -and $_.sentAsAlert -ne $true -and $_.Severity -in $severity - } + $LogTable = Get-CIPPTable $StandardsTable = Get-CIPPTable -tablename CippStandardsAlerts - $CurrentStandardsLogs = Get-CIPPAzDataTableEntity @StandardsTable -Filter $Filter | Where-Object { - $_.sentAsAlert -ne $true - } - Write-Information "Alerts: $($Currentlog.count) found" - Write-Information "Standards: $($CurrentStandardsLogs.count) found" + $PartitionKey = Get-Date -UFormat '%Y%m%d' + + # Server-side: sentAsAlert + severity (small fixed set). API is filtered client-side + # because the API list is open-ended and OR-expanding it can exceed the OData filter limit. + $sevOr = ($severity | ForEach-Object { "Severity eq '$($_ -replace "'", "''")'" }) -join ' or ' + $LogFilter = "PartitionKey eq '$PartitionKey' and sentAsAlert eq false and ($sevOr)" + $StandardsFilter = "PartitionKey eq '$PartitionKey' and sentAsAlert eq false" + + $Currentlog = @(Get-CIPPAzDataTableEntity @LogTable -Filter $LogFilter | Where-Object { $_.API -in $Settings }) + $CurrentStandardsLogs = @(Get-CIPPAzDataTableEntity @StandardsTable -Filter $StandardsFilter) + + Write-Information "Alerts: $($Currentlog.Count) found" + Write-Information "Standards: $($CurrentStandardsLogs.Count) found" # Get the CIPP URL $CippConfigTable = Get-CippTable -tablename Config $CippConfig = Get-CIPPAzDataTableEntity @CippConfigTable -Filter "PartitionKey eq 'InstanceProperties' and RowKey eq 'CIPPURL'" $CIPPURL = 'https://{0}' -f $CippConfig.Value - #email try + $LogsByTenant = @($Currentlog | Group-Object -Property Tenant) + $StandardsByTenant = @($CurrentStandardsLogs | Group-Object -Property Tenant) + + $MarkSent = { + param($Entities, $TargetTable) + if (-not $Entities -or $Entities.Count -eq 0) { return } + $batch = [System.Collections.Generic.List[object]]::new() + foreach ($e in $Entities) { + if ($e.PSObject.Properties.Name -contains 'sentAsAlert') { + $e.sentAsAlert = $true + } else { + $e | Add-Member -MemberType NoteProperty -Name sentAsAlert -Value $true -Force + } + $batch.Add($e) + if ($batch.Count -ge 100) { + Add-CIPPAzDataTableEntity @TargetTable -Entity $batch -Force + $batch.Clear() + } + } + if ($batch.Count -gt 0) { + Add-CIPPAzDataTableEntity @TargetTable -Entity $batch -Force + } + } + try { if ($Config.email -like '*@*') { - #Normal logs - if ($Currentlog) { + if ($Currentlog.Count -gt 0) { if ($config.onePerTenant) { - foreach ($tenant in ($CurrentLog.Tenant | Sort-Object -Unique)) { - $Data = ($CurrentLog | Select-Object Message, API, Tenant, Username, Severity | Where-Object -Property tenant -EQ $tenant) - $Subject = "$($Tenant): CIPP Alert: Alerts found starting at $((Get-Date).AddMinutes(-15))" + foreach ($g in $LogsByTenant) { + $tenant = $g.Name + $Data = $g.Group | Select-Object Message, API, Tenant, Username, Severity + $Subject = "$($tenant): CIPP Alert: Alerts found starting at $((Get-Date).AddMinutes(-15))" $HTMLContent = New-CIPPAlertTemplate -Data $Data -Format 'html' -InputObject 'table' -CIPPURL $CIPPURL Send-CIPPAlert -Type 'email' -Title $Subject -HTMLContent $HTMLContent.htmlcontent -TenantFilter $tenant -APIName 'Alerts' - $UpdateLogs = $CurrentLog | ForEach-Object { - if ($_.PSObject.Properties.Name -contains 'sentAsAlert') { - $_.sentAsAlert = $true - } else { - $_ | Add-Member -MemberType NoteProperty -Name sentAsAlert -Value $true -Force - } - $_ - } - if ($UpdateLogs) { - Add-CIPPAzDataTableEntity @Table -Entity $UpdateLogs -Force - } + & $MarkSent $g.Group $LogTable + $Data = $null; $HTMLContent = $null } } else { - $Data = ($CurrentLog | Select-Object Message, API, Tenant, Username, Severity) + $Data = $CurrentLog | Select-Object Message, API, Tenant, Username, Severity $Subject = "CIPP Alert: Alerts found starting at $((Get-Date).AddMinutes(-15))" $HTMLContent = New-CIPPAlertTemplate -Data $Data -Format 'html' -InputObject 'table' -CIPPURL $CIPPURL Send-CIPPAlert -Type 'email' -Title $Subject -HTMLContent $HTMLContent.htmlcontent -TenantFilter 'AllTenants' -APIName 'Alerts' - $UpdateLogs = $CurrentLog | ForEach-Object { - if ($_.PSObject.Properties.Name -contains 'sentAsAlert') { - $_.sentAsAlert = $true - } else { - $_ | Add-Member -MemberType NoteProperty -Name sentAsAlert -Value $true -Force - } - $_ - } - if ($UpdateLogs) { - Add-CIPPAzDataTableEntity @Table -Entity $UpdateLogs -Force - } + & $MarkSent $CurrentLog $LogTable + $Data = $null; $HTMLContent = $null } } - if ($CurrentStandardsLogs) { - foreach ($tenant in ($CurrentStandardsLogs.Tenant | Sort-Object -Unique)) { - $Data = ($CurrentStandardsLogs | Where-Object -Property tenant -EQ $tenant) - $Subject = "$($Tenant): Standards are out of sync for $tenant" + if ($CurrentStandardsLogs.Count -gt 0) { + foreach ($g in $StandardsByTenant) { + $tenant = $g.Name + $Data = $g.Group + $Subject = "$($tenant): Standards are out of sync for $tenant" $HTMLContent = New-CIPPAlertTemplate -Data $Data -Format 'html' -InputObject 'standards' -CIPPURL $CIPPURL Send-CIPPAlert -Type 'email' -Title $Subject -HTMLContent $HTMLContent.htmlcontent -TenantFilter $tenant -APIName 'Alerts' - $updateStandards = $CurrentStandardsLogs | ForEach-Object { - if ($_.PSObject.Properties.Name -contains 'sentAsAlert') { - $_.sentAsAlert = $true - } else { - $_ | Add-Member -MemberType NoteProperty -Name sentAsAlert -Value $true -Force - } - $_ - } - if ($updateStandards) { Add-CIPPAzDataTableEntity @StandardsTable -Entity $updateStandards -Force } + & $MarkSent $g.Group $StandardsTable + $Data = $null; $HTMLContent = $null } } } @@ -104,67 +106,61 @@ function Push-SchedulerCIPPNotifications { } try { - Write-Information $($config | ConvertTo-Json) Write-Information $config.webhook if (![string]::IsNullOrEmpty($config.webhook)) { - if ($Currentlog) { - $JSONContent = $Currentlog | ConvertTo-Json -Compress + $ChunkSize = 500 + if ($Currentlog.Count -gt 0) { $Title = "Logbook Notification: Alerts found starting at $((Get-Date).AddMinutes(-15))" - Send-CIPPAlert -Type 'webhook' -Title $Title -JSONContent $JSONContent -TenantFilter 'AllTenants' -APIName 'Alerts' -SchemaSource 'Logbook Notification' -InvokingCommand 'Push-SchedulerCIPPNotifications' -UseStandardizedSchema:$([boolean]$Config.UseStandardizedSchema) - $UpdateLogs = $CurrentLog | ForEach-Object { $_.sentAsAlert = $true; $_ } - if ($UpdateLogs) { Add-CIPPAzDataTableEntity @Table -Entity $UpdateLogs -Force } + for ($i = 0; $i -lt $Currentlog.Count; $i += $ChunkSize) { + $end = [math]::Min($i + $ChunkSize - 1, $Currentlog.Count - 1) + $chunk = $Currentlog[$i..$end] + $JSONContent = $chunk | ConvertTo-Json -Compress + Send-CIPPAlert -Type 'webhook' -Title $Title -JSONContent $JSONContent -TenantFilter 'AllTenants' -APIName 'Alerts' -SchemaSource 'Logbook Notification' -InvokingCommand 'Push-SchedulerCIPPNotifications' -UseStandardizedSchema:$([boolean]$Config.UseStandardizedSchema) + & $MarkSent $chunk $LogTable + $JSONContent = $null; $chunk = $null + } } - if ($CurrentStandardsLogs) { - $Data = $CurrentStandardsLogs - $JSONContent = New-CIPPAlertTemplate -Data $Data -Format 'json' -InputObject 'table' -CIPPURL $CIPPURL - $CurrentStandardsLogs | ConvertTo-Json -Compress - $Title = "Standards Notification: Out of sync standards detected" - Send-CIPPAlert -Type 'webhook' -Title $Title -JSONContent $JSONContent -TenantFilter 'AllTenants' -APIName 'Alerts' -SchemaSource 'Standards Notification' -InvokingCommand 'Push-SchedulerCIPPNotifications' -UseStandardizedSchema:$([boolean]$Config.UseStandardizedSchema) - $updateStandards = $CurrentStandardsLogs | ForEach-Object { - if ($_.PSObject.Properties.Name -contains 'sentAsAlert') { - $_.sentAsAlert = $true - } else { - $_ | Add-Member -MemberType NoteProperty -Name sentAsAlert -Value $true -Force - } - $_ + if ($CurrentStandardsLogs.Count -gt 0) { + $Title = 'Standards Notification: Out of sync standards detected' + for ($i = 0; $i -lt $CurrentStandardsLogs.Count; $i += $ChunkSize) { + $end = [math]::Min($i + $ChunkSize - 1, $CurrentStandardsLogs.Count - 1) + $chunk = $CurrentStandardsLogs[$i..$end] + $JSONContent = New-CIPPAlertTemplate -Data $chunk -Format 'json' -InputObject 'table' -CIPPURL $CIPPURL + Send-CIPPAlert -Type 'webhook' -Title $Title -JSONContent $JSONContent -TenantFilter 'AllTenants' -APIName 'Alerts' -SchemaSource 'Standards Notification' -InvokingCommand 'Push-SchedulerCIPPNotifications' -UseStandardizedSchema:$([boolean]$Config.UseStandardizedSchema) + & $MarkSent $chunk $StandardsTable + $JSONContent = $null; $chunk = $null } } - } } catch { Write-Information "Could not send alerts to webhook $($config.webhook): $($_.Exception.message)" - Write-LogMessage -API 'Alerts' -message "Could not send alerts to webhook $($config.webhook): $($_.Exception.message)" -tenant $Tenant -sev error -LogData (Get-CippException -Exception $_) + Write-LogMessage -API 'Alerts' -message "Could not send alerts to webhook $($config.webhook): $($_.Exception.message)" -tenant 'AllTenants' -sev error -LogData (Get-CippException -Exception $_) } if ($config.sendtoIntegration) { try { - foreach ($tenant in ($CurrentLog.Tenant | Sort-Object -Unique)) { - $Data = ($CurrentLog | Select-Object Message, API, Tenant, Username, Severity | Where-Object -Property tenant -EQ $tenant) + foreach ($g in $LogsByTenant) { + $tenant = $g.Name + $Data = $g.Group | Select-Object Message, API, Tenant, Username, Severity $HTMLContent = New-CIPPAlertTemplate -Data $Data -Format 'html' -InputObject 'table' -CIPPURL $CIPPURL $Title = "$tenant CIPP Alert: Alerts found starting at $((Get-Date).AddMinutes(-15))" Send-CIPPAlert -Type 'psa' -Title $Title -HTMLContent $HTMLContent.htmlcontent -TenantFilter $tenant -APIName 'Alerts' - $UpdateLogs = $CurrentLog | ForEach-Object { $_.SentAsAlert = $true; $_ } - if ($UpdateLogs) { Add-CIPPAzDataTableEntity @Table -Entity $UpdateLogs -Force } + & $MarkSent $g.Group $LogTable + $Data = $null; $HTMLContent = $null } - foreach ($standardsTenant in ($CurrentStandardsLogs.Tenant | Sort-Object -Unique)) { - $Data = ($CurrentStandardsLogs | Where-Object -Property tenant -EQ $standardsTenant) - $Subject = "$($standardsTenant): Standards are out of sync for $standardsTenant" + foreach ($g in $StandardsByTenant) { + $tenant = $g.Name + $Data = $g.Group + $Subject = "$($tenant): Standards are out of sync for $tenant" $HTMLContent = New-CIPPAlertTemplate -Data $Data -Format 'html' -InputObject 'standards' -CIPPURL $CIPPURL - Send-CIPPAlert -Type 'psa' -Title $Subject -HTMLContent $HTMLContent.htmlcontent -TenantFilter $standardsTenant -APIName 'Alerts' - $updateStandards = $CurrentStandardsLogs | ForEach-Object { - if ($_.PSObject.Properties.Name -contains 'sentAsAlert') { - $_.sentAsAlert = $true - } else { - $_ | Add-Member -MemberType NoteProperty -Name sentAsAlert -Value $true -Force - } - $_ - } + Send-CIPPAlert -Type 'psa' -Title $Subject -HTMLContent $HTMLContent.htmlcontent -TenantFilter $tenant -APIName 'Alerts' + & $MarkSent $g.Group $StandardsTable + $Data = $null; $HTMLContent = $null } } catch { Write-Information "Could not send alerts to ticketing system: $($_.Exception.message)" - Write-LogMessage -API 'Alerts' -tenant $Tenant -message "Could not send alerts to ticketing system: $($_.Exception.message)" -sev Error + Write-LogMessage -API 'Alerts' -tenant 'AllTenants' -message "Could not send alerts to ticketing system: $($_.Exception.message)" -sev Error } } - } diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListLogs.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListLogs.ps1 index 48389b215549..2cff4d234fbe 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListLogs.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListLogs.ps1 @@ -96,7 +96,6 @@ function Invoke-ListLogs { $EndDate = if ($Request.Query.EndDate ?? $Request.Query.DateFilter) { ConvertTo-CIPPODataFilterValue -Value ($Request.Query.EndDate ?? $Request.Query.DateFilter) -Type Date } else { $null } if ($StartDate -and $EndDate) { - # Collect logs for date range $Filter = "PartitionKey ge '$StartDate' and PartitionKey le '$EndDate'" } elseif ($StartDate) { $Filter = "PartitionKey eq '{0}'" -f $StartDate @@ -108,70 +107,82 @@ function Invoke-ListLogs { $PartitionKey = [TimeZoneInfo]::ConvertTimeBySystemTimeZoneId([DateTime]::UtcNow, $TzId).ToString('yyyyMMdd') $username = '*' $TenantFilter = $null + $ApiFilter = $null + $StandardFilter = $null + $ScheduledTaskFilter = $null $Filter = "PartitionKey eq '{0}'" -f $PartitionKey } - $AllowedTenants = Test-CIPPAccess -Request $Request -TenantList - Write-Host "Getting logs for filter: $Filter, LogLevel: $LogLevel, Username: $username" - $Rows = Get-AzDataTableEntity @Table -Filter $Filter | Where-Object { - $_.Severity -in $LogLevel -and - $_.Username -like $username -and - ([string]::IsNullOrEmpty($TenantFilter) -or $TenantFilter -eq 'AllTenants' -or $_.Tenant -like "*$TenantFilter*" -or $_.TenantID -eq $TenantFilter) -and - ([string]::IsNullOrEmpty($ApiFilter) -or $_.API -match "$ApiFilter") -and - ([string]::IsNullOrEmpty($StandardFilter) -or $_.StandardTemplateId -eq $StandardFilter) -and - ([string]::IsNullOrEmpty($ScheduledTaskFilter) -or $_.ScheduledTaskId -eq $ScheduledTaskFilter) + # Severity stays client-side: Azurite/Azure Table OData has been unreliable + # on long OR chains. Per-partition row counts are small enough that this is fine. + if ($StandardFilter) { + $SafeStd = ConvertTo-CIPPODataFilterValue -Value $StandardFilter -Type Guid + $Filter = "$Filter and StandardTemplateId eq '$SafeStd'" + } + if ($ScheduledTaskFilter) { + $SafeSched = ConvertTo-CIPPODataFilterValue -Value $ScheduledTaskFilter -Type Guid + $Filter = "$Filter and ScheduledTaskId eq '$SafeSched'" } + $AllowedTenants = Test-CIPPAccess -Request $Request -TenantList + Write-Host "Getting logs for filter: $Filter, LogLevel: $LogLevel, Username: $username" + if ($AllowedTenants -notcontains 'AllTenants') { $TenantList = Get-Tenants -IncludeErrors | Where-Object { $_.customerId -in $AllowedTenants } } - foreach ($Row in $Rows) { - if ($AllowedTenants -contains 'AllTenants' -or ($AllowedTenants -notcontains 'AllTenants' -and ($TenantList.defaultDomainName -contains $Row.Tenant -or $Row.Tenant -eq 'CIPP' -or $TenantList.customerId -contains $Row.TenantId)) ) { - if ($StandardTaskFilter -and $Row.StandardTemplateId) { - $Standard = ($Templates | Where-Object { $_.RowKey -eq $Row.StandardTemplateId }).JSON | ConvertFrom-Json - - $StandardInfo = @{ - Template = $Standard.templateName - Standard = $Row.Standard - } + $ReturnedLog = Get-AzDataTableEntity @Table -Filter $Filter | Where-Object { + $_.Severity -in $LogLevel -and + ($username -eq '*' -or $_.Username -like $username) -and + ([string]::IsNullOrEmpty($TenantFilter) -or $TenantFilter -eq 'AllTenants' -or $_.Tenant -like "*$TenantFilter*" -or $_.TenantID -eq $TenantFilter) -and + ([string]::IsNullOrEmpty($ApiFilter) -or $_.API -match "$ApiFilter") -and + ($AllowedTenants -contains 'AllTenants' -or $TenantList.defaultDomainName -contains $_.Tenant -or $_.Tenant -eq 'CIPP' -or $TenantList.customerId -contains $_.TenantId) + } | ForEach-Object { + $Row = $_ + if ($ScheduledTaskFilter -and $Row.StandardTemplateId) { + $Standard = ($Templates | Where-Object { $_.RowKey -eq $Row.StandardTemplateId }).JSON | ConvertFrom-Json + + $StandardInfo = @{ + Template = $Standard.templateName + Standard = $Row.Standard + } - if ($Row.IntuneTemplateId) { - $IntuneTemplate = ($Templates | Where-Object { $_.RowKey -eq $Row.IntuneTemplateId }).JSON | ConvertFrom-Json - $StandardInfo.IntunePolicy = $IntuneTemplate.displayName - } - if ($Row.ConditionalAccessTemplateId) { - $ConditionalAccessTemplate = ($Templates | Where-Object { $_.RowKey -eq $Row.ConditionalAccessTemplateId }).JSON | ConvertFrom-Json - $StandardInfo.ConditionalAccessPolicy = $ConditionalAccessTemplate.displayName - } - } else { - $StandardInfo = @{} + if ($Row.IntuneTemplateId) { + $IntuneTemplate = ($Templates | Where-Object { $_.RowKey -eq $Row.IntuneTemplateId }).JSON | ConvertFrom-Json + $StandardInfo.IntunePolicy = $IntuneTemplate.displayName } + if ($Row.ConditionalAccessTemplateId) { + $ConditionalAccessTemplate = ($Templates | Where-Object { $_.RowKey -eq $Row.ConditionalAccessTemplateId }).JSON | ConvertFrom-Json + $StandardInfo.ConditionalAccessPolicy = $ConditionalAccessTemplate.displayName + } + } else { + $StandardInfo = @{} + } - $LogData = if ($Row.LogData -and (Test-Json -Json $Row.LogData -ErrorAction SilentlyContinue)) { - $Row.LogData | ConvertFrom-Json - } else { $Row.LogData } - [PSCustomObject]@{ - DateTime = $Row.Timestamp - Tenant = $Row.Tenant - API = $Row.API - Message = $Row.Message - User = $Row.Username - Severity = $Row.Severity - LogData = $LogData - TenantID = if ($Row.TenantID -ne $null) { - $Row.TenantID - } else { - 'None' - } - AppId = $Row.AppId - IP = $Row.IP - RowKey = $Row.RowKey - StandardInfo = $StandardInfo - DateFilter = $Row.PartitionKey + $LogData = if ($Row.LogData -and (Test-Json -Json $Row.LogData -ErrorAction SilentlyContinue)) { + $Row.LogData | ConvertFrom-Json + } else { $Row.LogData } + [PSCustomObject]@{ + DateTime = $Row.Timestamp + Tenant = $Row.Tenant + API = $Row.API + Message = $Row.Message + User = $Row.Username + Severity = $Row.Severity + LogData = $LogData + TenantID = if ($Row.TenantID -ne $null) { + $Row.TenantID + } else { + 'None' } + AppId = $Row.AppId + IP = $Row.IP + RowKey = $Row.RowKey + StandardInfo = $StandardInfo + DateFilter = $Row.PartitionKey } } + $ReturnedLog } return [HttpResponseContext]@{ From 518855cc2af7527eea203002c643ef0a687caeb0 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Sat, 30 May 2026 23:27:13 +0800 Subject: [PATCH 081/202] test optimisation --- .../CIS/Identity/Invoke-CippTestCIS_1_2_2.ps1 | 14 +++++-- .../CIS/Identity/Invoke-CippTestCIS_2_1_5.ps1 | 4 +- .../CIS/Identity/Invoke-CippTestCIS_2_1_9.ps1 | 10 +++-- .../CIS/Identity/Invoke-CippTestCIS_4_2.ps1 | 4 +- .../Identity/Invoke-CippTestCIS_5_2_3_4.ps1 | 16 ++++++-- .../CIS/Identity/Invoke-CippTestCIS_8_1_1.ps1 | 12 +++--- .../Identity/Invoke-CippTestCISAMSEXO111.ps1 | 8 ++-- .../Invoke-CippTestCopilotReady015.ps1 | 16 ++++---- .../Identity/Invoke-CippTestEIDSCAAS04.ps1 | 4 +- .../Invoke-CippTestGenericTest004.ps1 | 26 +++++++++---- .../Invoke-CippTestGenericTest005.ps1 | 8 +++- .../Invoke-CippTestGenericTest006.ps1 | 8 +++- .../Invoke-CippTestGenericTest007.ps1 | 11 ++++-- .../Invoke-CippTestGenericTest008.ps1 | 11 ++++-- .../Invoke-CippTestGenericTest011.ps1 | 10 ++--- .../ORCA/Identity/Invoke-CippTestORCA104.ps1 | 9 ++--- .../ORCA/Identity/Invoke-CippTestORCA108.ps1 | 37 ++++++++++-------- .../ORCA/Identity/Invoke-CippTestORCA113.ps1 | 9 ++--- .../ORCA/Identity/Invoke-CippTestORCA239.ps1 | 20 +++++----- .../ZTNA/Devices/Invoke-CippTestZTNA24540.ps1 | 30 +++++++-------- .../ZTNA/Devices/Invoke-CippTestZTNA24550.ps1 | 34 ++++++++--------- .../ZTNA/Devices/Invoke-CippTestZTNA24552.ps1 | 34 ++++++++--------- .../Identity/Invoke-CippTestZTNA21773.ps1 | 14 ++++--- .../Identity/Invoke-CippTestZTNA21782.ps1 | 26 ++++++------- .../Identity/Invoke-CippTestZTNA21797.ps1 | 6 +-- .../Identity/Invoke-CippTestZTNA21858.ps1 | 33 ++++++++-------- .../Identity/Invoke-CippTestZTNA21868.ps1 | 38 ++++++++++--------- .../Identity/Invoke-CippTestZTNA21869.ps1 | 21 +++++----- .../Identity/Invoke-CippTestZTNA21877.ps1 | 25 ++++++------ .../Identity/Invoke-CippTestZTNA21886.ps1 | 25 ++++++------ .../Identity/Invoke-CippTestZTNA21896.ps1 | 23 ++++++----- .../Identity/Invoke-CippTestZTNA21992.ps1 | 29 +++++++------- .../Identity/Invoke-CippTestZTNA22128.ps1 | 31 ++++++++------- 33 files changed, 322 insertions(+), 284 deletions(-) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_2_2.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_2_2.ps1 index 4cfb17c8f705..c0d3ab48c41e 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_2_2.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_2_2.ps1 @@ -21,11 +21,19 @@ function Invoke-CippTestCIS_1_2_2 { return } - $EnabledShared = @() + $UsersById = @{} + $UsersByUpn = @{} + foreach ($U in $Users) { + if ($U.id) { $UsersById[$U.id] = $U } + if ($U.userPrincipalName) { $UsersByUpn[$U.userPrincipalName] = $U } + } + $EnabledShared = [System.Collections.Generic.List[object]]::new() foreach ($SM in $SharedMailboxes) { - $User = $Users | Where-Object { $_.userPrincipalName -eq $SM.UserPrincipalName -or $_.id -eq $SM.ExternalDirectoryObjectId } | Select-Object -First 1 + $User = $null + if ($SM.UserPrincipalName -and $UsersByUpn.ContainsKey($SM.UserPrincipalName)) { $User = $UsersByUpn[$SM.UserPrincipalName] } + elseif ($SM.ExternalDirectoryObjectId -and $UsersById.ContainsKey($SM.ExternalDirectoryObjectId)) { $User = $UsersById[$SM.ExternalDirectoryObjectId] } if ($User -and $User.accountEnabled -eq $true) { - $EnabledShared += $User + $EnabledShared.Add($User) } } diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_5.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_5.ps1 index 25d6134d0eb0..121a32a793ab 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_5.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_5.ps1 @@ -20,10 +20,10 @@ function Invoke-CippTestCIS_2_1_5 { EnableSafeDocs = $true AllowSafeDocsOpen = $false } - $Failures = @() + $Failures = [System.Collections.Generic.List[string]]::new() foreach ($key in $Required.Keys) { if ($Cfg.$key -ne $Required[$key]) { - $Failures += "$key = $($Cfg.$key) (expected $($Required[$key]))" + $Failures.Add("$key = $($Cfg.$key) (expected $($Required[$key]))") } } diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_9.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_9.ps1 index 7cb90eabd3c9..17868db8b874 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_9.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_9.ps1 @@ -14,12 +14,14 @@ function Invoke-CippTestCIS_2_1_9 { return } - $Sending = $Accepted | Where-Object { -not $_.SendingFromDomainDisabled -and $_.DomainName -notlike '*onmicrosoft.com' } - $Failed = @() + $Sending = $Accepted.Where({ -not $_.SendingFromDomainDisabled -and $_.DomainName -notlike '*onmicrosoft.com' }) + $DkimByDomain = $Dkim | Group-Object Domain -AsHashTable -AsString + $Failed = [System.Collections.Generic.List[object]]::new() foreach ($D in $Sending) { - $Cfg = $Dkim | Where-Object { $_.Domain -eq $D.DomainName } | Select-Object -First 1 + $Cfg = $null + if ($DkimByDomain.ContainsKey($D.DomainName)) { $Cfg = @($DkimByDomain[$D.DomainName])[0] } if (-not $Cfg -or $Cfg.Enabled -ne $true) { - $Failed += [PSCustomObject]@{ Domain = $D.DomainName; Enabled = $Cfg.Enabled } + $Failed.Add([PSCustomObject]@{ Domain = $D.DomainName; Enabled = $Cfg.Enabled }) } } diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_4_2.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_4_2.ps1 index c96bb67f1dce..964af9fc3390 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_4_2.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_4_2.ps1 @@ -21,11 +21,11 @@ function Invoke-CippTestCIS_4_2 { return } - $Failures = @() + $Failures = [System.Collections.Generic.List[string]]::new() foreach ($P in @('androidForWorkRestriction', 'androidRestriction', 'iosRestriction', 'macOSRestriction', 'windowsRestriction')) { $r = $DefaultPlatform.$P if ($r -and $r.personalDeviceEnrollmentBlocked -ne $true -and $r.platformBlocked -ne $true) { - $Failures += "$P : personal enrollment NOT blocked" + $Failures.Add("$P : personal enrollment NOT blocked") } } diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_4.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_4.ps1 index 6a9dfef02f6b..d8d2e8b8df21 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_4.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_4.ps1 @@ -14,12 +14,20 @@ function Invoke-CippTestCIS_5_2_3_4 { return } - $Members = $Users | Where-Object { $_.userType -eq 'Member' -and $_.accountEnabled -eq $true } - $NotCapable = @() + $Members = $Users.Where({ $_.userType -eq 'Member' -and $_.accountEnabled -eq $true }) + $RegById = @{} + $RegByUpn = @{} + foreach ($R in $Reg) { + if ($R.id) { $RegById[$R.id] = $R } + if ($R.userPrincipalName) { $RegByUpn[$R.userPrincipalName] = $R } + } + $NotCapable = [System.Collections.Generic.List[object]]::new() foreach ($U in $Members) { - $R = $Reg | Where-Object { $_.id -eq $U.id -or $_.userPrincipalName -eq $U.userPrincipalName } | Select-Object -First 1 + $R = $null + if ($U.id -and $RegById.ContainsKey($U.id)) { $R = $RegById[$U.id] } + elseif ($U.userPrincipalName -and $RegByUpn.ContainsKey($U.userPrincipalName)) { $R = $RegByUpn[$U.userPrincipalName] } if (-not $R -or $R.isMfaCapable -ne $true) { - $NotCapable += $U + $NotCapable.Add($U) } } diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_1_1.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_1_1.ps1 index 25c9ddca3b03..2183391c5117 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_1_1.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_1_1.ps1 @@ -14,12 +14,12 @@ function Invoke-CippTestCIS_8_1_1 { } $Cfg = $Client | Select-Object -First 1 - $Enabled = @() - if ($Cfg.AllowDropbox) { $Enabled += 'Dropbox' } - if ($Cfg.AllowBox) { $Enabled += 'Box' } - if ($Cfg.AllowGoogleDrive) { $Enabled += 'GoogleDrive' } - if ($Cfg.AllowShareFile) { $Enabled += 'ShareFile' } - if ($Cfg.AllowEgnyte) { $Enabled += 'Egnyte' } + $Enabled = [System.Collections.Generic.List[string]]::new() + if ($Cfg.AllowDropbox) { $Enabled.Add('Dropbox') } + if ($Cfg.AllowBox) { $Enabled.Add('Box') } + if ($Cfg.AllowGoogleDrive) { $Enabled.Add('GoogleDrive') } + if ($Cfg.AllowShareFile) { $Enabled.Add('ShareFile') } + if ($Cfg.AllowEgnyte) { $Enabled.Add('Egnyte') } if ($Enabled.Count -eq 0) { $Status = 'Passed' diff --git a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO111.ps1 b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO111.ps1 index 18c9d6fc0150..6aeb210c80c4 100644 --- a/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO111.ps1 +++ b/Modules/CIPPTests/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO111.ps1 @@ -28,10 +28,10 @@ function Invoke-CippTestCISAMSEXO111 { $StandardATP = $PresetPolicies | Where-Object { $_.Identity -like '*Preset Security Policy*' -and $_.ImpersonationProtectionState -eq 'Enabled' } - $EnabledPolicies = @() - if ($StandardEOP) { $EnabledPolicies += 'Standard EOP' } - if ($StrictEOP) { $EnabledPolicies += 'Strict EOP' } - if ($StandardATP) { $EnabledPolicies += "$($StandardATP.Count) ATP policy/policies with impersonation protection" } + $EnabledPolicies = [System.Collections.Generic.List[string]]::new() + if ($StandardEOP) { $EnabledPolicies.Add('Standard EOP') } + if ($StrictEOP) { $EnabledPolicies.Add('Strict EOP') } + if ($StandardATP) { $EnabledPolicies.Add("$($StandardATP.Count) ATP policy/policies with impersonation protection") } if ($EnabledPolicies.Count -gt 0) { $Result = "✅ **Pass**: Preset security policies with impersonation protection are enabled:`n`n" diff --git a/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady015.ps1 b/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady015.ps1 index 3bb45ac64ef9..94239d79ff02 100644 --- a/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady015.ps1 +++ b/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady015.ps1 @@ -20,9 +20,7 @@ function Invoke-CippTestCopilotReady015 { $ActiveUsers = @($UsageData | Where-Object { $_.userPrincipalName -and $_.userPrincipalName -ne 'Not applicable' }) if ($ActiveUsers.Count -eq 0) { - $Result = "No Microsoft 365 Copilot usage was detected in the past 30 days.`n`n" - $Result += 'This tenant either has no Copilot licenses assigned, or users have not yet started using Copilot features. ' - $Result += 'See tests CopilotReady001 and CopilotReady002 to check licensing status.' + $Result = "No Microsoft 365 Copilot usage was detected in the past 30 days.`n`nThis tenant either has no Copilot licenses assigned, or users have not yet started using Copilot features. See tests CopilotReady001 and CopilotReady002 to check licensing status." Add-CippTestResult -TenantFilter $Tenant -TestId 'CopilotReady015' -TestType 'Identity' -Status 'Informational' -ResultMarkdown $Result -Risk 'Informational' -Name 'Microsoft 365 Copilot usage per user' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Copilot Readiness' return } @@ -33,12 +31,13 @@ function Invoke-CippTestCopilotReady015 { $_ -notin @('userPrincipalName', 'displayName', 'lastActivityDate', 'reportRefreshDate', 'reportPeriod', 'id') } - $Result = "**$($ActiveUsers.Count) users** had Copilot activity in the past 30 days.`n`n" + $sb = [System.Text.StringBuilder]::new() + $null = $sb.Append("**$($ActiveUsers.Count) users** had Copilot activity in the past 30 days.`n`n") # Build table header from available columns $Headers = @('User', 'Last Active') + $AppColumns - $Result += '| ' + ($Headers -join ' | ') + " |`n" - $Result += '| ' + (($Headers | ForEach-Object { '---' }) -join ' | ') + " |`n" + $null = $sb.Append('| ' + ($Headers -join ' | ') + " |`n") + $null = $sb.Append('| ' + (($Headers | ForEach-Object { '---' }) -join ' | ') + " |`n") $DisplayUsers = $ActiveUsers | Sort-Object lastActivityDate -Descending | Select-Object -First 50 foreach ($User in $DisplayUsers) { @@ -48,12 +47,13 @@ function Invoke-CippTestCopilotReady015 { $Val = $User.$Col $null = $Row.Append(" $Val |") } - $Result += "$Row`n" + $null = $sb.Append("$Row`n") } if ($ActiveUsers.Count -gt 50) { - $Result += "`n*Showing 50 of $($ActiveUsers.Count) active users.*" + $null = $sb.Append("`n*Showing 50 of $($ActiveUsers.Count) active users.*") } + $Result = $sb.ToString() Add-CippTestResult -TenantFilter $Tenant -TestId 'CopilotReady015' -TestType 'Identity' -Status 'Informational' -ResultMarkdown $Result -Risk 'Informational' -Name 'Microsoft 365 Copilot usage per user' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Copilot Readiness' diff --git a/Modules/CIPPTests/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAS04.ps1 b/Modules/CIPPTests/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAS04.ps1 index fda96e4850df..25dfc5584a24 100644 --- a/Modules/CIPPTests/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAS04.ps1 +++ b/Modules/CIPPTests/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAS04.ps1 @@ -20,11 +20,11 @@ function Invoke-CippTestEIDSCAAS04 { return } - $InvalidTargets = @() + $InvalidTargets = [System.Collections.Generic.List[string]]::new() if ($SmsConfig.includeTargets) { foreach ($target in $SmsConfig.includeTargets) { if ($target.isUsableForSignIn -ne $false) { - $InvalidTargets += $target.id + $InvalidTargets.Add($target.id) } } } diff --git a/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest004.ps1 b/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest004.ps1 index 4d6b19eaeee2..653390b5d916 100644 --- a/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest004.ps1 +++ b/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest004.ps1 @@ -20,13 +20,25 @@ function Invoke-CippTestGenericTest004 { } $TotalUsers = $Users.Count - $MFARegistered = @($Users | Where-Object { $_.MFARegistration -eq $true }).Count - $MFACapable = @($Users | Where-Object { $_.MFACapable -eq $true }).Count - $CoveredByCA = @($Users | Where-Object { $_.CoveredByCA -like 'Enforced*' }).Count - $CoveredBySD = @($Users | Where-Object { $_.CoveredBySD -eq $true }).Count - $PerUserMFA = @($Users | Where-Object { $_.PerUser -in @('Enforced', 'Enabled') }).Count - $NotProtected = @($Users | Where-Object { $_.CoveredByCA -notlike 'Enforced*' -and $_.CoveredBySD -ne $true -and $_.PerUser -notin @('Enforced', 'Enabled') }).Count - $AdminCount = @($Users | Where-Object { $_.IsAdmin -eq $true }).Count + $MFARegistered = 0 + $MFACapable = 0 + $CoveredByCA = 0 + $CoveredBySD = 0 + $PerUserMFA = 0 + $NotProtected = 0 + $AdminCount = 0 + foreach ($u in $Users) { + if ($u.MFARegistration -eq $true) { $MFARegistered++ } + if ($u.MFACapable -eq $true) { $MFACapable++ } + $isCA = $u.CoveredByCA -like 'Enforced*' + $isSD = $u.CoveredBySD -eq $true + $isPerUser = $u.PerUser -in @('Enforced', 'Enabled') + if ($isCA) { $CoveredByCA++ } + if ($isSD) { $CoveredBySD++ } + if ($isPerUser) { $PerUserMFA++ } + if (-not $isCA -and -not $isSD -and -not $isPerUser) { $NotProtected++ } + if ($u.IsAdmin -eq $true) { $AdminCount++ } + } $MFARegPct = if ($TotalUsers -gt 0) { [math]::Round(($MFARegistered / $TotalUsers) * 100, 1) } else { 0 } $Result = [System.Text.StringBuilder]::new("### Summary`n`n") diff --git a/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest005.ps1 b/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest005.ps1 index 6f25700f1eba..b8f653383985 100644 --- a/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest005.ps1 +++ b/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest005.ps1 @@ -22,8 +22,12 @@ function Invoke-CippTestGenericTest005 { } $TotalAdmins = $Admins.Count - $MFARegistered = @($Admins | Where-Object { $_.MFARegistration -eq $true }).Count - $NotProtected = @($Admins | Where-Object { $_.CoveredByCA -notlike 'Enforced*' -and $_.CoveredBySD -ne $true -and $_.PerUser -notin @('Enforced', 'Enabled') }).Count + $MFARegistered = 0 + $NotProtected = 0 + foreach ($a in $Admins) { + if ($a.MFARegistration -eq $true) { $MFARegistered++ } + if ($a.CoveredByCA -notlike 'Enforced*' -and $a.CoveredBySD -ne $true -and $a.PerUser -notin @('Enforced', 'Enabled')) { $NotProtected++ } + } $MFARegPct = if ($TotalAdmins -gt 0) { [math]::Round(($MFARegistered / $TotalAdmins) * 100, 1) } else { 0 } $Result = [System.Text.StringBuilder]::new("**Total Admins:** $TotalAdmins | **MFA Registered:** $MFARegistered ($MFARegPct%)") diff --git a/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest006.ps1 b/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest006.ps1 index dee9fb7994df..4b47956736b0 100644 --- a/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest006.ps1 +++ b/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest006.ps1 @@ -22,8 +22,12 @@ function Invoke-CippTestGenericTest006 { } $TotalUsers = $StandardUsers.Count - $MFARegistered = @($StandardUsers | Where-Object { $_.MFARegistration -eq $true }).Count - $NotProtected = @($StandardUsers | Where-Object { $_.CoveredByCA -notlike 'Enforced*' -and $_.CoveredBySD -ne $true -and $_.PerUser -notin @('Enforced', 'Enabled') }).Count + $MFARegistered = 0 + $NotProtected = 0 + foreach ($u in $StandardUsers) { + if ($u.MFARegistration -eq $true) { $MFARegistered++ } + if ($u.CoveredByCA -notlike 'Enforced*' -and $u.CoveredBySD -ne $true -and $u.PerUser -notin @('Enforced', 'Enabled')) { $NotProtected++ } + } $MFARegPct = if ($TotalUsers -gt 0) { [math]::Round(($MFARegistered / $TotalUsers) * 100, 1) } else { 0 } $Result = [System.Text.StringBuilder]::new("**Total Users:** $TotalUsers | **MFA Registered:** $MFARegistered ($MFARegPct%)") diff --git a/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest007.ps1 b/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest007.ps1 index 3d4d71b98fd3..8347f3c4af64 100644 --- a/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest007.ps1 +++ b/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest007.ps1 @@ -22,9 +22,14 @@ function Invoke-CippTestGenericTest007 { } $TotalUsers = $LicensedUsers.Count - $MFARegistered = @($LicensedUsers | Where-Object { $_.MFARegistration -eq $true }).Count - $NotProtected = @($LicensedUsers | Where-Object { $_.CoveredByCA -notlike 'Enforced*' -and $_.CoveredBySD -ne $true -and $_.PerUser -notin @('Enforced', 'Enabled') }).Count - $Admins = @($LicensedUsers | Where-Object { $_.IsAdmin -eq $true }).Count + $MFARegistered = 0 + $NotProtected = 0 + $Admins = 0 + foreach ($u in $LicensedUsers) { + if ($u.MFARegistration -eq $true) { $MFARegistered++ } + if ($u.CoveredByCA -notlike 'Enforced*' -and $u.CoveredBySD -ne $true -and $u.PerUser -notin @('Enforced', 'Enabled')) { $NotProtected++ } + if ($u.IsAdmin -eq $true) { $Admins++ } + } $MFARegPct = if ($TotalUsers -gt 0) { [math]::Round(($MFARegistered / $TotalUsers) * 100, 1) } else { 0 } $Result = [System.Text.StringBuilder]::new("**Licensed Users:** $TotalUsers | **Admins among them:** $Admins | **MFA Registered:** $MFARegistered ($MFARegPct%)") diff --git a/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest008.ps1 b/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest008.ps1 index f082405ac151..d3b01114042d 100644 --- a/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest008.ps1 +++ b/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest008.ps1 @@ -25,9 +25,14 @@ function Invoke-CippTestGenericTest008 { return } - $EnforcedCount = @($PerUserMFAUsers | Where-Object { $_.PerUser -eq 'Enforced' }).Count - $EnabledCount = @($PerUserMFAUsers | Where-Object { $_.PerUser -eq 'Enabled' }).Count - $AdminsAffected = @($PerUserMFAUsers | Where-Object { $_.IsAdmin -eq $true }).Count + $EnforcedCount = 0 + $EnabledCount = 0 + $AdminsAffected = 0 + foreach ($u in $PerUserMFAUsers) { + if ($u.PerUser -eq 'Enforced') { $EnforcedCount++ } + if ($u.PerUser -eq 'Enabled') { $EnabledCount++ } + if ($u.IsAdmin -eq $true) { $AdminsAffected++ } + } $null = $Result.Append("### Current Status`n`n") $null = $Result.Append("**⚠️ $($PerUserMFAUsers.Count) account(s) are still using legacy Per-User MFA.**`n`n") diff --git a/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest011.ps1 b/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest011.ps1 index ac3e9105eea4..816fe0b820de 100644 --- a/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest011.ps1 +++ b/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest011.ps1 @@ -155,11 +155,11 @@ function Invoke-CippTestGenericTest011 { continue } - # Split into categories - $CompliantItems = @($Details | Where-Object { $_.ComplianceStatus -eq 'Compliant' }) - $NonCompliantItems = @($Details | Where-Object { $_.ComplianceStatus -eq 'Non-Compliant' }) - $LicenseMissingItems = @($Details | Where-Object { $_.ComplianceStatus -eq 'License Missing' }) - $ReportingDisabledItems = @($Details | Where-Object { $_.ComplianceStatus -eq 'Reporting Disabled' }) + $ByStatus = $Details | Group-Object ComplianceStatus -AsHashTable -AsString + $CompliantItems = @($ByStatus['Compliant']) + $NonCompliantItems = @($ByStatus['Non-Compliant']) + $LicenseMissingItems = @($ByStatus['License Missing']) + $ReportingDisabledItems = @($ByStatus['Reporting Disabled']) # Helper to resolve and skip unresolvable template items $TemplateSettings = $Template.standardSettings diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA104.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA104.ps1 index da2cd06bfc06..2382b39b8301 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA104.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA104.ps1 @@ -13,15 +13,14 @@ function Invoke-CippTestORCA104 { return } - $FailedPolicies = @() - $PassedPolicies = @() + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + $PassedPolicies = [System.Collections.Generic.List[object]]::new() foreach ($Policy in $AntiPhishPolicies) { - # Check if HighConfidencePhishAction is set to Quarantine if ($Policy.HighConfidencePhishAction -eq 'Quarantine') { - $PassedPolicies += $Policy + $PassedPolicies.Add($Policy) } else { - $FailedPolicies += $Policy + $FailedPolicies.Add($Policy) } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA108.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA108.ps1 index f46e2031e374..145e63a7f4e8 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA108.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA108.ps1 @@ -14,41 +14,46 @@ function Invoke-CippTestORCA108 { return } - # Get custom domains (exclude default .onmicrosoft.com domains) - $CustomDomains = $AcceptedDomains | Where-Object { + $CustomDomains = $AcceptedDomains.Where({ $_.DomainName -notlike '*.onmicrosoft.com' -and $_.DomainName -notlike '*.mail.onmicrosoft.com' - } + }) if ($CustomDomains.Count -eq 0) { $Status = 'Passed' $Result = 'No custom domains configured. DKIM check not applicable for default domains only.' } else { - $DomainsWithoutDkim = @() - $DomainsWithDkim = @() + $DkimByDomain = $DkimConfig | Group-Object Domain -AsHashTable -AsString + $DomainsWithoutDkim = [System.Collections.Generic.List[string]]::new() + $DomainsWithDkim = [System.Collections.Generic.List[string]]::new() foreach ($Domain in $CustomDomains) { - $DkimForDomain = $DkimConfig | Where-Object { $_.Domain -eq $Domain.DomainName } + $DkimForDomain = $null + if ($DkimByDomain -and $DkimByDomain.ContainsKey($Domain.DomainName)) { $DkimForDomain = @($DkimByDomain[$Domain.DomainName])[0] } if ($DkimForDomain -and $DkimForDomain.Enabled -eq $true) { - $DomainsWithDkim += $Domain.DomainName + $DomainsWithDkim.Add($Domain.DomainName) } else { - $DomainsWithoutDkim += $Domain.DomainName + $DomainsWithoutDkim.Add($Domain.DomainName) } } if ($DomainsWithoutDkim.Count -eq 0) { $Status = 'Passed' - $Result = "DKIM signing is enabled for all custom domains ($($DomainsWithDkim.Count) domains).`n`n" - $Result += "**Domains with DKIM enabled:**`n" - $Result += ($DomainsWithDkim | ForEach-Object { "- $_" }) -join "`n" + $sb = [System.Text.StringBuilder]::new() + $null = $sb.Append("DKIM signing is enabled for all custom domains ($($DomainsWithDkim.Count) domains).`n`n") + $null = $sb.Append("**Domains with DKIM enabled:**`n") + $null = $sb.Append(($DomainsWithDkim | ForEach-Object { "- $_" }) -join "`n") + $Result = $sb.ToString() } else { $Status = 'Failed' - $Result = "DKIM signing is not configured for all custom domains.`n`n" - $Result += "**Missing DKIM:** $($DomainsWithoutDkim.Count) | **Configured:** $($DomainsWithDkim.Count)`n`n" - $Result += "### Domains without DKIM:`n" - $Result += ($DomainsWithoutDkim | ForEach-Object { "- $_" }) -join "`n" - $Result += "`n`n**Remediation:** Enable DKIM signing for all custom domains to prevent email spoofing." + $sb = [System.Text.StringBuilder]::new() + $null = $sb.Append("DKIM signing is not configured for all custom domains.`n`n") + $null = $sb.Append("**Missing DKIM:** $($DomainsWithoutDkim.Count) | **Configured:** $($DomainsWithDkim.Count)`n`n") + $null = $sb.Append("### Domains without DKIM:`n") + $null = $sb.Append(($DomainsWithoutDkim | ForEach-Object { "- $_" }) -join "`n") + $null = $sb.Append("`n`n**Remediation:** Enable DKIM signing for all custom domains to prevent email spoofing.") + $Result = $sb.ToString() } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA113.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA113.ps1 index a6f5d30b681b..af98497bee2a 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA113.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA113.ps1 @@ -13,15 +13,14 @@ function Invoke-CippTestORCA113 { return } - $FailedPolicies = @() - $PassedPolicies = @() + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + $PassedPolicies = [System.Collections.Generic.List[object]]::new() foreach ($Policy in $SafeLinksPolicies) { - # Check if DoNotAllowClickThrough is set to true (which means AllowClickThrough is disabled) if ($Policy.DoNotAllowClickThrough -eq $true) { - $PassedPolicies += $Policy + $PassedPolicies.Add($Policy) } else { - $FailedPolicies += $Policy + $FailedPolicies.Add($Policy) } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA239.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA239.ps1 index 414b918df87f..33ff5c3246d5 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA239.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA239.ps1 @@ -14,27 +14,27 @@ function Invoke-CippTestORCA239 { return } - $FailedPolicies = @() - $Issues = @() + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + $Issues = [System.Collections.Generic.List[string]]::new() # Check Anti-Phish policies for exclusions if ($AntiPhishPolicies) { foreach ($Policy in $AntiPhishPolicies) { $HasExclusions = $false - $ExclusionDetails = @() + $ExclusionDetails = [System.Collections.Generic.List[string]]::new() if ($Policy.ExcludedSenders -and $Policy.ExcludedSenders.Count -gt 0) { $HasExclusions = $true - $ExclusionDetails += "ExcludedSenders: $($Policy.ExcludedSenders.Count)" + $ExclusionDetails.Add("ExcludedSenders: $($Policy.ExcludedSenders.Count)") } if ($Policy.ExcludedDomains -and $Policy.ExcludedDomains.Count -gt 0) { $HasExclusions = $true - $ExclusionDetails += "ExcludedDomains: $($Policy.ExcludedDomains.Count)" + $ExclusionDetails.Add("ExcludedDomains: $($Policy.ExcludedDomains.Count)") } if ($HasExclusions) { - $Issues += "Anti-Phish Policy '$($Policy.Identity)': $($ExclusionDetails -join ', ')" + $Issues.Add("Anti-Phish Policy '$($Policy.Identity)': $($ExclusionDetails -join ', ')") } } } @@ -43,20 +43,20 @@ function Invoke-CippTestORCA239 { if ($ContentFilterPolicies) { foreach ($Policy in $ContentFilterPolicies) { $HasExclusions = $false - $ExclusionDetails = @() + $ExclusionDetails = [System.Collections.Generic.List[string]]::new() if ($Policy.AllowedSenders -and $Policy.AllowedSenders.Count -gt 0) { $HasExclusions = $true - $ExclusionDetails += "AllowedSenders: $($Policy.AllowedSenders.Count)" + $ExclusionDetails.Add("AllowedSenders: $($Policy.AllowedSenders.Count)") } if ($Policy.AllowedSenderDomains -and $Policy.AllowedSenderDomains.Count -gt 0) { $HasExclusions = $true - $ExclusionDetails += "AllowedSenderDomains: $($Policy.AllowedSenderDomains.Count)" + $ExclusionDetails.Add("AllowedSenderDomains: $($Policy.AllowedSenderDomains.Count)") } if ($HasExclusions) { - $Issues += "Anti-Spam Policy '$($Policy.Identity)': $($ExclusionDetails -join ', ')" + $Issues.Add("Anti-Spam Policy '$($Policy.Identity)': $($ExclusionDetails -join ', ')") } } } diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24540.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24540.ps1 index 8e1e153bb04f..9b0789dea207 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24540.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24540.ps1 @@ -27,14 +27,13 @@ function Invoke-CippTestZTNA24540 { if ($AssignedPolicies.Count -gt 0) { $Status = 'Passed' - $ResultLines = @( - 'At least one Windows Firewall policy is created and assigned to a group.' - '' - '**Windows Firewall Configuration Policies:**' - '' - '| Policy Name | Status | Assignment Count |' - '| :---------- | :----- | :--------------- |' - ) + $ResultLines = [System.Collections.Generic.List[string]]::new() + $ResultLines.Add('At least one Windows Firewall policy is created and assigned to a group.') + $ResultLines.Add('') + $ResultLines.Add('**Windows Firewall Configuration Policies:**') + $ResultLines.Add('') + $ResultLines.Add('| Policy Name | Status | Assignment Count |') + $ResultLines.Add('| :---------- | :----- | :--------------- |') foreach ($Policy in $FirewallPolicies) { $PolicyStatus = if ($Policy.assignments -and $Policy.assignments.Count -gt 0) { @@ -43,21 +42,20 @@ function Invoke-CippTestZTNA24540 { '❌ Not assigned' } $AssignmentCount = if ($Policy.assignments) { $Policy.assignments.Count } else { 0 } - $ResultLines += "| $($Policy.name) | $PolicyStatus | $AssignmentCount |" + $ResultLines.Add("| $($Policy.name) | $PolicyStatus | $AssignmentCount |") } $Result = $ResultLines -join "`n" } else { $Status = 'Failed' - $ResultLines = @( - 'There are no firewall policies assigned to any groups.' - '' - '**Windows Firewall Configuration Policies (Unassigned):**' - '' - ) + $ResultLines = [System.Collections.Generic.List[string]]::new() + $ResultLines.Add('There are no firewall policies assigned to any groups.') + $ResultLines.Add('') + $ResultLines.Add('**Windows Firewall Configuration Policies (Unassigned):**') + $ResultLines.Add('') foreach ($Policy in $FirewallPolicies) { - $ResultLines += "- $($Policy.name)" + $ResultLines.Add("- $($Policy.name)") } $Result = $ResultLines -join "`n" diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24550.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24550.ps1 index b8bef5ee4307..f0a06b7eacb9 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24550.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24550.ps1 @@ -17,7 +17,7 @@ function Invoke-CippTestZTNA24550 { $_.platforms -match 'windows10' } - $WindowsBitLockerPolicies = @() + $WindowsBitLockerPolicies = [System.Collections.Generic.List[object]]::new() foreach ($WindowsPolicy in $WindowsPolicies) { $ValidSettingValues = @('device_vendor_msft_bitlocker_requiredeviceencryption_1') @@ -36,7 +36,7 @@ function Invoke-CippTestZTNA24550 { } if ($HasValidSetting) { - $WindowsBitLockerPolicies += $WindowsPolicy + $WindowsBitLockerPolicies.Add($WindowsPolicy) } } } @@ -47,14 +47,13 @@ function Invoke-CippTestZTNA24550 { if ($AssignedPolicies.Count -gt 0) { $Status = 'Passed' - $ResultLines = @( - 'At least one Windows BitLocker policy is configured and assigned.' - '' - '**Windows BitLocker Policies:**' - '' - '| Policy Name | Status | Assignment Count |' - '| :---------- | :----- | :--------------- |' - ) + $ResultLines = [System.Collections.Generic.List[string]]::new() + $ResultLines.Add('At least one Windows BitLocker policy is configured and assigned.') + $ResultLines.Add('') + $ResultLines.Add('**Windows BitLocker Policies:**') + $ResultLines.Add('') + $ResultLines.Add('| Policy Name | Status | Assignment Count |') + $ResultLines.Add('| :---------- | :----- | :--------------- |') foreach ($Policy in $WindowsBitLockerPolicies) { $PolicyStatus = if ($Policy.assignments -and $Policy.assignments.Count -gt 0) { @@ -63,21 +62,20 @@ function Invoke-CippTestZTNA24550 { '❌ Not assigned' } $AssignmentCount = if ($Policy.assignments) { $Policy.assignments.Count } else { 0 } - $ResultLines += "| $($Policy.name) | $PolicyStatus | $AssignmentCount |" + $ResultLines.Add("| $($Policy.name) | $PolicyStatus | $AssignmentCount |") } $Result = $ResultLines -join "`n" } else { $Status = 'Failed' if ($WindowsBitLockerPolicies.Count -gt 0) { - $ResultLines = @( - 'Windows BitLocker policies exist but none are assigned.' - '' - '**Unassigned BitLocker Policies:**' - '' - ) + $ResultLines = [System.Collections.Generic.List[string]]::new() + $ResultLines.Add('Windows BitLocker policies exist but none are assigned.') + $ResultLines.Add('') + $ResultLines.Add('**Unassigned BitLocker Policies:**') + $ResultLines.Add('') foreach ($Policy in $WindowsBitLockerPolicies) { - $ResultLines += "- $($Policy.name)" + $ResultLines.Add("- $($Policy.name)") } } else { $ResultLines = @('No Windows BitLocker policy is configured or assigned.') diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24552.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24552.ps1 index 047942d7ef86..39e4712f7d29 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24552.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24552.ps1 @@ -17,7 +17,7 @@ function Invoke-CippTestZTNA24552 { $_.platforms -match 'macOS' } - $MacOSFirewallPolicies = @() + $MacOSFirewallPolicies = [System.Collections.Generic.List[object]]::new() foreach ($MacOSPolicy in $MacOSPolicies) { $ValidSettingValues = @('com.apple.security.firewall_enablefirewall_true') @@ -36,7 +36,7 @@ function Invoke-CippTestZTNA24552 { } if ($HasValidSetting) { - $MacOSFirewallPolicies += $MacOSPolicy + $MacOSFirewallPolicies.Add($MacOSPolicy) } } } @@ -47,14 +47,13 @@ function Invoke-CippTestZTNA24552 { if ($AssignedPolicies.Count -gt 0) { $Status = 'Passed' - $ResultLines = @( - 'At least one macOS Firewall policy is configured and assigned.' - '' - '**macOS Firewall Policies:**' - '' - '| Policy Name | Status | Assignment Count |' - '| :---------- | :----- | :--------------- |' - ) + $ResultLines = [System.Collections.Generic.List[string]]::new() + $ResultLines.Add('At least one macOS Firewall policy is configured and assigned.') + $ResultLines.Add('') + $ResultLines.Add('**macOS Firewall Policies:**') + $ResultLines.Add('') + $ResultLines.Add('| Policy Name | Status | Assignment Count |') + $ResultLines.Add('| :---------- | :----- | :--------------- |') foreach ($Policy in $MacOSFirewallPolicies) { $PolicyStatus = if ($Policy.assignments -and $Policy.assignments.Count -gt 0) { @@ -63,21 +62,20 @@ function Invoke-CippTestZTNA24552 { '❌ Not assigned' } $AssignmentCount = if ($Policy.assignments) { $Policy.assignments.Count } else { 0 } - $ResultLines += "| $($Policy.name) | $PolicyStatus | $AssignmentCount |" + $ResultLines.Add("| $($Policy.name) | $PolicyStatus | $AssignmentCount |") } $Result = $ResultLines -join "`n" } else { $Status = 'Failed' if ($MacOSFirewallPolicies.Count -gt 0) { - $ResultLines = @( - 'macOS Firewall policies exist but none are assigned.' - '' - '**Unassigned Firewall Policies:**' - '' - ) + $ResultLines = [System.Collections.Generic.List[string]]::new() + $ResultLines.Add('macOS Firewall policies exist but none are assigned.') + $ResultLines.Add('') + $ResultLines.Add('**Unassigned Firewall Policies:**') + $ResultLines.Add('') foreach ($Policy in $MacOSFirewallPolicies) { - $ResultLines += "- $($Policy.name)" + $ResultLines.Add("- $($Policy.name)") } } else { $ResultLines = @('No macOS Firewall policy is configured or assigned.') diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21773.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21773.ps1 index 2f0ed70e02c4..ef373619a5c2 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21773.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21773.ps1 @@ -65,18 +65,20 @@ function Invoke-CippTestZTNA21773 { $Result = 'Applications in your tenant do not have certificates valid for more than 180 days' } else { $Status = 'Failed' - $Result = "Found $($AppsWithLongCerts.Count) applications and $($SPsWithLongCerts.Count) service principals with certificates longer than 180 days`n`n" + $sb = [System.Text.StringBuilder]::new() + $null = $sb.Append("Found $($AppsWithLongCerts.Count) applications and $($SPsWithLongCerts.Count) service principals with certificates longer than 180 days`n`n") if ($AppsWithLongCerts.Count -gt 0) { - $Result += "## Apps with long-lived certificates:`n`n" - $Result += ($AppsWithLongCerts | ForEach-Object { "- $($_.displayName) (AppId: $($_.appId))" }) -join "`n" - $Result += "`n`n" + $null = $sb.Append("## Apps with long-lived certificates:`n`n") + $null = $sb.Append((($AppsWithLongCerts | ForEach-Object { "- $($_.displayName) (AppId: $($_.appId))" }) -join "`n")) + $null = $sb.Append("`n`n") } if ($SPsWithLongCerts.Count -gt 0) { - $Result += "## Service principals with long-lived certificates:`n`n" - $Result += ($SPsWithLongCerts | ForEach-Object { "- $($_.displayName) (AppId: $($_.appId))" }) -join "`n" + $null = $sb.Append("## Service principals with long-lived certificates:`n`n") + $null = $sb.Append((($SPsWithLongCerts | ForEach-Object { "- $($_.displayName) (AppId: $($_.appId))" }) -join "`n")) } + $Result = $sb.ToString() } Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21773' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Applications do not have certificates with expiration longer than 180 days' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application Management' diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21782.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21782.ps1 index dc190d978943..738136e66f24 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21782.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21782.ps1 @@ -16,15 +16,12 @@ function Invoke-CippTestZTNA21782 { $PhishResistantMethods = @('passKeyDeviceBound', 'passKeyDeviceBoundAuthenticator', 'windowsHelloForBusiness') - # Join user registration details with role assignments - $results = $UserRegistrationDetails | Where-Object { - $userId = $_.id - $RoleAssignments | Where-Object { $_.principalId -eq $userId } - } | ForEach-Object { - $user = $_ - $userRoles = $RoleAssignments | Where-Object { $_.principalId -eq $user.id } + $RoleAssignmentsByPrincipal = $RoleAssignments | Group-Object principalId -AsHashTable -AsString + $results = [System.Collections.Generic.List[object]]::new() + foreach ($user in $UserRegistrationDetails) { + if (-not $RoleAssignmentsByPrincipal.ContainsKey($user.id)) { continue } + $userRoles = $RoleAssignmentsByPrincipal[$user.id] $hasPhishResistant = $false - if ($user.methodsRegistered) { foreach ($method in $PhishResistantMethods) { if ($user.methodsRegistered -contains $method) { @@ -33,21 +30,20 @@ function Invoke-CippTestZTNA21782 { } } } - - [PSCustomObject]@{ + $results.Add([PSCustomObject]@{ id = $user.id userDisplayName = $user.userDisplayName roleDisplayName = ($userRoles.roleDefinitionName -join ', ') methodsRegistered = $user.methodsRegistered phishResistantAuthMethod = $hasPhishResistant - } + }) } - $totalUserCount = $results.Length - $phishResistantPrivUsers = $results | Where-Object { $_.phishResistantAuthMethod } - $phishablePrivUsers = $results | Where-Object { !$_.phishResistantAuthMethod } + $totalUserCount = $results.Count + $phishResistantPrivUsers = $results.Where({ $_.phishResistantAuthMethod }) + $phishablePrivUsers = $results.Where({ !$_.phishResistantAuthMethod }) - $phishResistantPrivUserCount = $phishResistantPrivUsers.Length + $phishResistantPrivUserCount = $phishResistantPrivUsers.Count $passed = $totalUserCount -eq $phishResistantPrivUserCount diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21797.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21797.ps1 index 5c82edcc4e51..7cba73ae4c14 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21797.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21797.ps1 @@ -33,7 +33,7 @@ function Invoke-CippTestZTNA21797 { } $passwordlessEnabled = $false - $passwordlessAuthMethods = @() + $passwordlessAuthMethods = [System.Collections.Generic.List[object]]::new() if ($authMethodsPolicy.authenticationMethodConfigurations) { foreach ($method in $authMethodsPolicy.authenticationMethodConfigurations) { @@ -55,11 +55,11 @@ function Invoke-CippTestZTNA21797 { if ($isPasswordless) { $passwordlessEnabled = $true - $passwordlessAuthMethods += [PSCustomObject]@{ + $passwordlessAuthMethods.Add([PSCustomObject]@{ Name = $methodName State = $methodState AdditionalInfo = $additionalInfo - } + }) } } } diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21858.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21858.ps1 index 0038f540e3f1..79006a05d989 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21858.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21858.ps1 @@ -14,14 +14,14 @@ function Invoke-CippTestZTNA21858 { $InactivityThresholdDays = 90 $Today = Get-Date - $EnabledGuests = $Guests | Where-Object { $_.AccountEnabled -eq $true } + $EnabledGuests = $Guests.Where({ $_.AccountEnabled -eq $true }) if (-not $EnabledGuests) { Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21858' -TestType 'Identity' -Status 'Passed' -ResultMarkdown 'No guest users found in the tenant' -Risk 'Medium' -Name 'Inactive guest identities are disabled or removed from the tenant' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'External collaboration' return } - $InactiveGuests = @() + $InactiveGuests = [System.Collections.Generic.List[object]]::new() foreach ($Guest in $EnabledGuests) { $DaysSinceLastActivity = $null @@ -34,22 +34,21 @@ function Invoke-CippTestZTNA21858 { } if ($null -ne $DaysSinceLastActivity -and $DaysSinceLastActivity -gt $InactivityThresholdDays) { - $InactiveGuests += $Guest + $InactiveGuests.Add($Guest) } } if ($InactiveGuests.Count -gt 0) { $Status = 'Failed' - $ResultLines = @( - "Found $($InactiveGuests.Count) inactive guest user(s) with no sign-in activity in the last $InactivityThresholdDays days." - '' - "**Total enabled guests:** $($EnabledGuests.Count)" - "**Inactive guests:** $($InactiveGuests.Count)" - "**Inactivity threshold:** $InactivityThresholdDays days" - '' - '**Top 10 inactive guest users:**' - ) + $ResultLines = [System.Collections.Generic.List[string]]::new() + $ResultLines.Add("Found $($InactiveGuests.Count) inactive guest user(s) with no sign-in activity in the last $InactivityThresholdDays days.") + $ResultLines.Add('') + $ResultLines.Add("**Total enabled guests:** $($EnabledGuests.Count)") + $ResultLines.Add("**Inactive guests:** $($InactiveGuests.Count)") + $ResultLines.Add("**Inactivity threshold:** $InactivityThresholdDays days") + $ResultLines.Add('') + $ResultLines.Add('**Top 10 inactive guest users:**') $Top10Guests = $InactiveGuests | Sort-Object { if ($_.signInActivity.lastSuccessfulSignInDateTime) { @@ -63,20 +62,20 @@ function Invoke-CippTestZTNA21858 { if ($Guest.signInActivity.lastSuccessfulSignInDateTime) { $LastActivity = [DateTime]$Guest.signInActivity.lastSuccessfulSignInDateTime $DaysInactive = [Math]::Round(($Today - $LastActivity).TotalDays, 0) - $ResultLines += "- $($Guest.displayName) ($($Guest.userPrincipalName)) - Last sign-in: $DaysInactive days ago" + $ResultLines.Add("- $($Guest.displayName) ($($Guest.userPrincipalName)) - Last sign-in: $DaysInactive days ago") } else { $Created = [DateTime]$Guest.createdDateTime $DaysSinceCreated = [Math]::Round(($Today - $Created).TotalDays, 0) - $ResultLines += "- $($Guest.displayName) ($($Guest.userPrincipalName)) - Never signed in (Created $DaysSinceCreated days ago)" + $ResultLines.Add("- $($Guest.displayName) ($($Guest.userPrincipalName)) - Never signed in (Created $DaysSinceCreated days ago)") } } if ($InactiveGuests.Count -gt 10) { - $ResultLines += "- ... and $($InactiveGuests.Count - 10) more inactive guest(s)" + $ResultLines.Add("- ... and $($InactiveGuests.Count - 10) more inactive guest(s)") } - $ResultLines += '' - $ResultLines += '**Recommendation:** Review and remove or disable inactive guest accounts to reduce security risks.' + $ResultLines.Add('') + $ResultLines.Add('**Recommendation:** Review and remove or disable inactive guest accounts to reduce security risks.') $Result = $ResultLines -join "`n" } else { diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21868.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21868.ps1 index 78428bf1ca43..0df9b441fcef 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21868.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21868.ps1 @@ -68,29 +68,31 @@ function Invoke-CippTestZTNA21868 { if ($HasGuestAppOwners -or $HasGuestSpOwners) { $Status = 'Failed' - $Result = "Guest users own applications or service principals`n`n" + $sb = [System.Text.StringBuilder]::new() + $null = $sb.Append("Guest users own applications or service principals`n`n") if ($HasGuestAppOwners -and $HasGuestSpOwners) { - $Result += "## Guest users own both applications and service principals`n`n" - $Result += "### Applications owned by guest users`n`n" - $Result += "| User Display Name | User Principal Name | Application |`n" - $Result += "| :---------------- | :------------------ | :---------- |`n" - $Result += ($GuestAppOwners | ForEach-Object { "| $($_.displayName) | $($_.userPrincipalName) | $($_.appDisplayName) |" }) -join "`n" - $Result += "`n`n### Service principals owned by guest users`n`n" - $Result += "| User Display Name | User Principal Name | Service Principal |`n" - $Result += "| :---------------- | :------------------ | :---------------- |`n" - $Result += ($GuestSpOwners | ForEach-Object { "| $($_.displayName) | $($_.userPrincipalName) | $($_.spDisplayName) |" }) -join "`n" + $null = $sb.Append("## Guest users own both applications and service principals`n`n") + $null = $sb.Append("### Applications owned by guest users`n`n") + $null = $sb.Append("| User Display Name | User Principal Name | Application |`n") + $null = $sb.Append("| :---------------- | :------------------ | :---------- |`n") + $null = $sb.Append((($GuestAppOwners | ForEach-Object { "| $($_.displayName) | $($_.userPrincipalName) | $($_.appDisplayName) |" }) -join "`n")) + $null = $sb.Append("`n`n### Service principals owned by guest users`n`n") + $null = $sb.Append("| User Display Name | User Principal Name | Service Principal |`n") + $null = $sb.Append("| :---------------- | :------------------ | :---------------- |`n") + $null = $sb.Append((($GuestSpOwners | ForEach-Object { "| $($_.displayName) | $($_.userPrincipalName) | $($_.spDisplayName) |" }) -join "`n")) } elseif ($HasGuestAppOwners) { - $Result += "## Guest users own applications`n`n" - $Result += "| User Display Name | User Principal Name | Application |`n" - $Result += "| :---------------- | :------------------ | :---------- |`n" - $Result += ($GuestAppOwners | ForEach-Object { "| $($_.displayName) | $($_.userPrincipalName) | $($_.appDisplayName) |" }) -join "`n" + $null = $sb.Append("## Guest users own applications`n`n") + $null = $sb.Append("| User Display Name | User Principal Name | Application |`n") + $null = $sb.Append("| :---------------- | :------------------ | :---------- |`n") + $null = $sb.Append((($GuestAppOwners | ForEach-Object { "| $($_.displayName) | $($_.userPrincipalName) | $($_.appDisplayName) |" }) -join "`n")) } elseif ($HasGuestSpOwners) { - $Result += "## Guest users own service principals`n`n" - $Result += "| User Display Name | User Principal Name | Service Principal |`n" - $Result += "| :---------------- | :------------------ | :---------------- |`n" - $Result += ($GuestSpOwners | ForEach-Object { "| $($_.displayName) | $($_.userPrincipalName) | $($_.spDisplayName) |" }) -join "`n" + $null = $sb.Append("## Guest users own service principals`n`n") + $null = $sb.Append("| User Display Name | User Principal Name | Service Principal |`n") + $null = $sb.Append("| :---------------- | :------------------ | :---------------- |`n") + $null = $sb.Append((($GuestSpOwners | ForEach-Object { "| $($_.displayName) | $($_.userPrincipalName) | $($_.spDisplayName) |" }) -join "`n")) } + $Result = $sb.ToString() } else { $Status = 'Passed' $Result = 'No guest users own any applications or service principals in the tenant' diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21869.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21869.ps1 index a6463412b080..9c3f471a94f8 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21869.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21869.ps1 @@ -26,25 +26,24 @@ function Invoke-CippTestZTNA21869 { $Status = 'Investigate' - $ResultLines = @( - "Found $($AppsWithoutAssignment.Count) enterprise application(s) without assignment requirements." - '' - '**Applications without user assignment requirements:**' - ) + $ResultLines = [System.Collections.Generic.List[string]]::new() + $ResultLines.Add("Found $($AppsWithoutAssignment.Count) enterprise application(s) without assignment requirements.") + $ResultLines.Add('') + $ResultLines.Add('**Applications without user assignment requirements:**') $Top10Apps = $AppsWithoutAssignment | Select-Object -First 10 foreach ($App in $Top10Apps) { - $ResultLines += "- $($App.displayName) (SSO: $($App.preferredSingleSignOnMode))" + $ResultLines.Add("- $($App.displayName) (SSO: $($App.preferredSingleSignOnMode))") } if ($AppsWithoutAssignment.Count -gt 10) { - $ResultLines += "- ... and $($AppsWithoutAssignment.Count - 10) more application(s)" + $ResultLines.Add("- ... and $($AppsWithoutAssignment.Count - 10) more application(s)") } - $ResultLines += '' - $ResultLines += '**Note:** Full provisioning scope validation requires Graph API synchronization endpoint not available in cache.' - $ResultLines += '' - $ResultLines += '**Recommendation:** Enable user assignment requirements or configure scoped provisioning to limit application access.' + $ResultLines.Add('') + $ResultLines.Add('**Note:** Full provisioning scope validation requires Graph API synchronization endpoint not available in cache.') + $ResultLines.Add('') + $ResultLines.Add('**Recommendation:** Enable user assignment requirements or configure scoped provisioning to limit application access.') $Result = $ResultLines -join "`n" diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21877.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21877.ps1 index 4d71643c7f4a..994f79eb9bf4 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21877.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21877.ps1 @@ -25,27 +25,26 @@ function Invoke-CippTestZTNA21877 { } else { $Status = 'Failed' - $ResultLines = @( - "Found $($GuestsWithoutSponsors.Count) guest user(s) without sponsors out of $($Guests.Count) total guests." - '' - "**Total guests:** $($Guests.Count)" - "**Guests without sponsors:** $($GuestsWithoutSponsors.Count)" - "**Guests with sponsors:** $($Guests.Count - $GuestsWithoutSponsors.Count)" - '' - '**Top 10 guests without sponsors:**' - ) + $ResultLines = [System.Collections.Generic.List[string]]::new() + $ResultLines.Add("Found $($GuestsWithoutSponsors.Count) guest user(s) without sponsors out of $($Guests.Count) total guests.") + $ResultLines.Add('') + $ResultLines.Add("**Total guests:** $($Guests.Count)") + $ResultLines.Add("**Guests without sponsors:** $($GuestsWithoutSponsors.Count)") + $ResultLines.Add("**Guests with sponsors:** $($Guests.Count - $GuestsWithoutSponsors.Count)") + $ResultLines.Add('') + $ResultLines.Add('**Top 10 guests without sponsors:**') $Top10Guests = $GuestsWithoutSponsors | Select-Object -First 10 foreach ($Guest in $Top10Guests) { - $ResultLines += "- $($Guest.displayName) ($($Guest.userPrincipalName))" + $ResultLines.Add("- $($Guest.displayName) ($($Guest.userPrincipalName))") } if ($GuestsWithoutSponsors.Count -gt 10) { - $ResultLines += "- ... and $($GuestsWithoutSponsors.Count - 10) more guest(s)" + $ResultLines.Add("- ... and $($GuestsWithoutSponsors.Count - 10) more guest(s)") } - $ResultLines += '' - $ResultLines += '**Recommendation:** Assign sponsors to all guest accounts for better accountability and lifecycle management.' + $ResultLines.Add('') + $ResultLines.Add('**Recommendation:** Assign sponsors to all guest accounts for better accountability and lifecycle management.') $Result = $ResultLines -join "`n" } diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21886.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21886.ps1 index 030a60f394e9..45d331f64871 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21886.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21886.ps1 @@ -25,29 +25,28 @@ function Invoke-CippTestZTNA21886 { $Status = 'Investigate' - $ResultLines = @( - "Found $($AppsWithSSO.Count) application(s) configured for SSO." - '' - '**Applications with SSO enabled:**' - ) + $ResultLines = [System.Collections.Generic.List[string]]::new() + $ResultLines.Add("Found $($AppsWithSSO.Count) application(s) configured for SSO.") + $ResultLines.Add('') + $ResultLines.Add('**Applications with SSO enabled:**') $SSOByType = $AppsWithSSO | Group-Object -Property preferredSingleSignOnMode foreach ($Group in $SSOByType) { - $ResultLines += '' - $ResultLines += "**$($Group.Name.ToUpper()) SSO** ($($Group.Count) app(s)):" + $ResultLines.Add('') + $ResultLines.Add("**$($Group.Name.ToUpper()) SSO** ($($Group.Count) app(s)):") $Top5 = $Group.Group | Select-Object -First 5 foreach ($App in $Top5) { - $ResultLines += "- $($App.displayName)" + $ResultLines.Add("- $($App.displayName)") } if ($Group.Count -gt 5) { - $ResultLines += "- ... and $($Group.Count - 5) more" + $ResultLines.Add("- ... and $($Group.Count - 5) more") } } - $ResultLines += '' - $ResultLines += '**Note:** Provisioning template and job validation requires Graph API synchronization endpoint not available in cache.' - $ResultLines += '' - $ResultLines += '**Recommendation:** Configure automatic user provisioning for applications that support it to ensure consistent access management.' + $ResultLines.Add('') + $ResultLines.Add('**Note:** Provisioning template and job validation requires Graph API synchronization endpoint not available in cache.') + $ResultLines.Add('') + $ResultLines.Add('**Recommendation:** Configure automatic user provisioning for applications that support it to ensure consistent access management.') $Result = $ResultLines -join "`n" diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21896.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21896.ps1 index 125185f05142..e2b896dad462 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21896.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21896.ps1 @@ -28,25 +28,24 @@ function Invoke-CippTestZTNA21896 { $TotalWithCreds = $SPsWithPassCreds.Count + $SPsWithKeyCreds.Count $Status = 'Investigate' - $ResultLines = @( - "Found $TotalWithCreds service principal(s) with credentials configured in the tenant, which represents a security risk." - '' - ) + $ResultLines = [System.Collections.Generic.List[string]]::new() + $ResultLines.Add("Found $TotalWithCreds service principal(s) with credentials configured in the tenant, which represents a security risk.") + $ResultLines.Add('') if ($SPsWithPassCreds.Count -gt 0) { - $ResultLines += "**Service principals with password credentials:** $($SPsWithPassCreds.Count)" - $ResultLines += '' + $ResultLines.Add("**Service principals with password credentials:** $($SPsWithPassCreds.Count)") + $ResultLines.Add('') } if ($SPsWithKeyCreds.Count -gt 0) { - $ResultLines += "**Service principals with key credentials (certificates):** $($SPsWithKeyCreds.Count)" - $ResultLines += '' + $ResultLines.Add("**Service principals with key credentials (certificates):** $($SPsWithKeyCreds.Count)") + $ResultLines.Add('') } - $ResultLines += '**Security implications:**' - $ResultLines += '- Service principals with credentials can be compromised if not properly secured' - $ResultLines += '- Password credentials are less secure than managed identities or certificate-based authentication' - $ResultLines += '- Consider using managed identities where possible to eliminate credential management' + $ResultLines.Add('**Security implications:**') + $ResultLines.Add('- Service principals with credentials can be compromised if not properly secured') + $ResultLines.Add('- Password credentials are less secure than managed identities or certificate-based authentication') + $ResultLines.Add('- Consider using managed identities where possible to eliminate credential management') $Result = $ResultLines -join "`n" diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21992.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21992.ps1 index eadb787f29c7..61b276337c13 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21992.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21992.ps1 @@ -68,40 +68,39 @@ function Invoke-CippTestZTNA21992 { $Status = 'Failed' - $ResultLines = @( - "Found $($OldAppCerts.Count) application(s) and $($OldSPCerts.Count) service principal(s) with certificates not rotated within $RotationThresholdDays days." - '' - "**Certificate rotation threshold:** $RotationThresholdDays days" - '' - ) + $ResultLines = [System.Collections.Generic.List[string]]::new() + $ResultLines.Add("Found $($OldAppCerts.Count) application(s) and $($OldSPCerts.Count) service principal(s) with certificates not rotated within $RotationThresholdDays days.") + $ResultLines.Add('') + $ResultLines.Add("**Certificate rotation threshold:** $RotationThresholdDays days") + $ResultLines.Add('') if ($OldAppCerts.Count -gt 0) { - $ResultLines += '**Applications with old certificates:**' + $ResultLines.Add('**Applications with old certificates:**') $Top10Apps = $OldAppCerts | Select-Object -First 10 foreach ($App in $Top10Apps) { $DaysOld = [Math]::Round(((Get-Date) - $App.OldestCertDate).TotalDays, 0) - $ResultLines += "- $($App.DisplayName) (Certificate age: $DaysOld days)" + $ResultLines.Add("- $($App.DisplayName) (Certificate age: $DaysOld days)") } if ($OldAppCerts.Count -gt 10) { - $ResultLines += "- ... and $($OldAppCerts.Count - 10) more application(s)" + $ResultLines.Add("- ... and $($OldAppCerts.Count - 10) more application(s)") } - $ResultLines += '' + $ResultLines.Add('') } if ($OldSPCerts.Count -gt 0) { - $ResultLines += '**Service principals with old certificates:**' + $ResultLines.Add('**Service principals with old certificates:**') $Top10SPs = $OldSPCerts | Select-Object -First 10 foreach ($SP in $Top10SPs) { $DaysOld = [Math]::Round(((Get-Date) - $SP.OldestCertDate).TotalDays, 0) - $ResultLines += "- $($SP.DisplayName) (Certificate age: $DaysOld days)" + $ResultLines.Add("- $($SP.DisplayName) (Certificate age: $DaysOld days)") } if ($OldSPCerts.Count -gt 10) { - $ResultLines += "- ... and $($OldSPCerts.Count - 10) more service principal(s)" + $ResultLines.Add("- ... and $($OldSPCerts.Count - 10) more service principal(s)") } - $ResultLines += '' + $ResultLines.Add('') } - $ResultLines += '**Recommendation:** Rotate certificates regularly to reduce the risk of credential compromise.' + $ResultLines.Add('**Recommendation:** Rotate certificates regularly to reduce the risk of credential compromise.') $Result = $ResultLines -join "`n" diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA22128.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA22128.ps1 index e002c3d2d2f7..e9773a34e7bd 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA22128.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA22128.ps1 @@ -36,17 +36,17 @@ function Invoke-CippTestZTNA22128 { 'fe930be7-5e62-47db-91af-98c3a49a38b1' ) - $GuestsInPrivilegedRoles = @() + $GuestsInPrivilegedRoles = [System.Collections.Generic.List[object]]::new() foreach ($Role in $Roles) { if ($Role.roleTemplateId -in $PrivilegedRoleTemplateIds -and $Role.members) { foreach ($Member in $Role.members) { if ($GuestIdHash.ContainsKey($Member.id)) { - $GuestsInPrivilegedRoles += [PSCustomObject]@{ + $GuestsInPrivilegedRoles.Add([PSCustomObject]@{ RoleName = $Role.displayName GuestId = $Member.id GuestDisplayName = $Member.displayName GuestUserPrincipalName = $Member.userPrincipalName - } + }) } } } @@ -59,26 +59,25 @@ function Invoke-CippTestZTNA22128 { $Status = 'Failed' - $ResultLines = @( - "Found $($GuestsInPrivilegedRoles.Count) guest user(s) with privileged role assignments." - '' - "**Total guests in tenant:** $($Guests.Count)" - "**Guests with privileged roles:** $($GuestsInPrivilegedRoles.Count)" - '' - '**Guest users in privileged roles:**' - ) + $ResultLines = [System.Collections.Generic.List[string]]::new() + $ResultLines.Add("Found $($GuestsInPrivilegedRoles.Count) guest user(s) with privileged role assignments.") + $ResultLines.Add('') + $ResultLines.Add("**Total guests in tenant:** $($Guests.Count)") + $ResultLines.Add("**Guests with privileged roles:** $($GuestsInPrivilegedRoles.Count)") + $ResultLines.Add('') + $ResultLines.Add('**Guest users in privileged roles:**') $RoleGroups = $GuestsInPrivilegedRoles | Group-Object -Property RoleName foreach ($RoleGroup in $RoleGroups) { - $ResultLines += '' - $ResultLines += "**$($RoleGroup.Name)** ($($RoleGroup.Count) guest(s)):" + $ResultLines.Add('') + $ResultLines.Add("**$($RoleGroup.Name)** ($($RoleGroup.Count) guest(s)):") foreach ($Guest in $RoleGroup.Group) { - $ResultLines += "- $($Guest.GuestDisplayName) ($($Guest.GuestUserPrincipalName))" + $ResultLines.Add("- $($Guest.GuestDisplayName) ($($Guest.GuestUserPrincipalName))") } } - $ResultLines += '' - $ResultLines += '**Security concern:** Guest users should not have privileged directory roles. Consider using separate admin accounts for external administrators or removing privileged access.' + $ResultLines.Add('') + $ResultLines.Add('**Security concern:** Guest users should not have privileged directory roles. Consider using separate admin accounts for external administrators or removing privileged access.') $Result = $ResultLines -join "`n" From 999f0283f8a7e273cd34c48978b6ff8cefef7ddb Mon Sep 17 00:00:00 2001 From: Chris Dewey <142454021+chris-dewey-1991@users.noreply.github.com> Date: Sun, 31 May 2026 12:20:16 +0100 Subject: [PATCH 082/202] Fix TenantAllowBlockListTemplate always reporting non-compliant Standard was missing report and alert blocks so compliance state was never recorded. Added both blocks with correct Expected/Current Configuration output showing template name, list type, and Allow/Block action. Remediate block unchanged. Signed-off-by: Chris Dewey <142454021+chris-dewey-1991@users.noreply.github.com> --- ...PPStandardTenantAllowBlockListTemplate.ps1 | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTenantAllowBlockListTemplate.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTenantAllowBlockListTemplate.ps1 index 5efb88b9210d..7acb886435e8 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTenantAllowBlockListTemplate.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTenantAllowBlockListTemplate.ps1 @@ -121,4 +121,46 @@ function Invoke-CIPPStandardTenantAllowBlockListTemplate { } } } + + $MissingByTemplate = @(foreach ($TemplateData in $ResolvedTemplates) { + $Entries = @($TemplateData.entries -split '[,;]' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | ForEach-Object { $_.Trim() }) + $ExistingItems = try { New-ExoRequest -tenantid $Tenant -cmdlet 'Get-TenantAllowBlockListItems' -cmdParams @{ ListType = $TemplateData.listType } } catch { @() } + $ExistingValues = @($ExistingItems | Select-Object -ExpandProperty Value) + $MissingEntries = @($Entries | Where-Object { $ExistingValues -notcontains $_ }) + if ($MissingEntries.Count -gt 0) { + [PSCustomObject]@{ + TemplateName = $TemplateData.templateName + ListType = $TemplateData.listType + Action = $TemplateData.listMethod + MissingEntries = $MissingEntries + } + } + }) + + $StateIsCorrect = $MissingByTemplate.Count -eq 0 + + if ($Settings.alert -eq $true) { + if ($StateIsCorrect) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'All Tenant Allow/Block List template entries are present' -sev Info + } else { + $MissingSummary = ($MissingByTemplate | ForEach-Object { "$($_.TemplateName) [$($_.ListType)/$($_.Action)]: $($_.MissingEntries -join ', ')" }) -join '; ' + Write-StandardsAlert -message "Tenant Allow/Block List entries are missing: $MissingSummary" -object $MissingByTemplate -tenant $Tenant -standardName 'TenantAllowBlockListTemplate' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Tenant Allow/Block List entries are missing: $MissingSummary" -sev Info + } + } + + if ($Settings.report -eq $true) { + if ($StateIsCorrect) { + $CurrentValue = 'All template entries are present' + $ExpectedValue = 'All template entries are present' + } else { + $ExpectedValue = ($ResolvedTemplates | ForEach-Object { + $Entries = @($_.entries -split '[,;]' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | ForEach-Object { $_.Trim() }) + "$($_.templateName) [$($_.listType)/$($_.listMethod)]: $($Entries -join ', ')" + }) -join '; ' + $CurrentValue = ($MissingByTemplate | ForEach-Object { "$($_.TemplateName) [$($_.ListType)/$($_.Action)] - Missing: $($_.MissingEntries -join ', ')" }) -join '; ' + } + Add-CIPPBPAField -FieldName 'TenantAllowBlockListTemplate' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.TenantAllowBlockListTemplate' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant + } } From 7b341600ea66916a6393af4882ed9e2e55a58a51 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 31 May 2026 22:52:53 +0200 Subject: [PATCH 083/202] update openapi spec with generated one --- openapi.json | 38707 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 31588 insertions(+), 7119 deletions(-) diff --git a/openapi.json b/openapi.json index 194c1d67d499..a0c8a4fa087d 100644 --- a/openapi.json +++ b/openapi.json @@ -1,9429 +1,33765 @@ { - "externalDocs": { - "url": "https://cipp.app", - "description": "CIPP Documentation" - }, + "openapi": "3.1.0", "info": { - "version": "1.0.1", - "description": "CIPP-API is an Azure Function App operating as the logic layer for the CIPP platform. It is composed primarily of standard Azure Functions with a handful of Azure Durable Functions handling more complex actions (mostly applying standards and running tenant analysis). API documentation is primarily intended to aid in further development of the CIPP platform. This API will most likely be out-dated and we request users to help us keep this up to date.", - "title": "CIPP-API Documentation" + "title": "CIPP API", + "version": "auto", + "description": "CIPP-API is an Azure Function App providing the logic layer for the CIPP platform. This spec is auto-generated via static analysis of both the API (PowerShell) and frontend (React/Next.js) repositories.", + "x-cipp-docs": "https://docs.cipp.app" }, - "paths": { - "/AddGroup": { - "post": { - "description": "AddGroup", - "summary": "AddGroup", - "tags": ["POST"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "selectedTenants", - "in": "body" + "servers": [ + { + "url": "/api", + "description": "CIPP API" + } + ], + "security": [ + { + "bearerAuth": [] + } + ], + "components": { + "schemas": { + "LabelValue": { + "type": "object", + "description": "Autocomplete/select field. Most dropdowns in CIPP use this shape. Backend typically unwraps via: $x.value ?? $x", + "properties": { + "label": { + "type": "string" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "tenantid", - "in": "body" + "value": { + "type": "string" } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" - } + } + }, + "LabelValueNumber": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "value": { + "type": "number" + } + } + }, + "GroupRef": { + "type": "object", + "description": "Reference to a group, including type metadata for routing (Graph vs EXO).", + "properties": { + "label": { + "type": "string" + }, + "value": { + "type": "string", + "description": "Group object ID" + }, + "addedFields": { + "type": "object", + "properties": { + "groupType": { + "type": "string", + "enum": [ + "Security", + "Distribution list", + "Mail-Enabled Security", + "Microsoft 365" + ] } + } + } + } + }, + "PostExecution": { + "type": "object", + "description": "Notification channels triggered after task completion.", + "properties": { + "webhook": { + "type": "boolean" + }, + "email": { + "type": "boolean" + }, + "psa": { + "type": "boolean" + } + } + }, + "ScheduledTask": { + "type": "object", + "description": "Controls deferred execution. If Enabled is true, task is queued rather than run immediately.", + "properties": { + "Enabled": { + "type": "boolean" + }, + "date": { + "type": "string", + "format": "date-time" + } + } + }, + "StandardResults": { + "type": "object", + "description": "Standard CIPP API response envelope.", + "properties": { + "Results": { + "type": "array", + "items": { + "type": "string" }, - "description": "Successful operation" + "description": "Result messages, one per operation. Mix of success and error strings." } } + }, + "DynamicExtensionFields": { + "type": "object", + "description": "Per-tenant custom directory extension attributes. Schema varies by tenant configuration and cannot be statically defined.", + "additionalProperties": true, + "x-cipp-dynamic": true + } + }, + "parameters": { + "tenantFilter": { + "name": "tenantFilter", + "in": "query", + "description": "Target tenant domain or 'AllTenants' for multi-tenant operations.", + "required": true, + "schema": { + "type": "string" + } + }, + "selectedTenants": { + "name": "selectedTenants", + "in": "query", + "description": "Comma-separated list of tenant domains for bulk operations.", + "required": false, + "schema": { + "type": "string" + } } }, - "/AddChocoApp": { + "securitySchemes": { + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT", + "description": "Azure AD bearer token. Obtained via MSAL against the CIPP API app registration." + } + } + }, + "paths": { + "/api/AddAPDevice": { "post": { - "description": "AddChocoApp", - "summary": "AddChocoApp", - "tags": ["POST"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "InstallationIntent", - "in": "body" - }, + "summary": "AddAPDevice", + "tags": [ + "Endpoint > Autopilot" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "AssignTo", - "in": "body" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Adds Autopilot devices to a tenant via Partner Center API\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", + "x-cipp-role": "Endpoint.Autopilot.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "autopilotData": { + "type": "string" + }, + "Groupname": { + "type": "string" + }, + "TenantFilter": { + "type": "string" + } + }, + "required": [ + "TenantFilter" + ] + } + } } } } }, - "/AddWin32ScriptApp": { + "/api/AddAlert": { "post": { - "description": "AddWin32ScriptApp", - "summary": "AddWin32ScriptApp", - "tags": ["POST"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "ApplicationName", - "in": "body" - }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "installScript", - "in": "body" - }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "AssignTo", - "in": "body" - }, + "summary": "AddAlert", + "tags": [ + "Tenant > Administration > Alerts" + ], + "security": [ { - "required": false, - "schema": { - "type": "string" - }, - "name": "InstallationIntent", - "in": "body" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Alert.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "actions": { + "type": "array", + "items": { + "type": "string" + } + }, + "AlertComment": { + "type": "string" + }, + "command": { + "$ref": "#/components/schemas/LabelValue" + }, + "conditions": { + "type": "string" + }, + "excludedTenants": { + "type": "string" + }, + "logbook": { + "$ref": "#/components/schemas/LabelValue" + }, + "postExecution": { + "type": "array", + "items": { + "type": "string" + } + }, + "preset": { + "$ref": "#/components/schemas/LabelValue" + }, + "recurrence": { + "$ref": "#/components/schemas/LabelValue" + }, + "RowKey": { + "type": "string" + }, + "startDateTime": { + "type": "string", + "format": "date-time" + }, + "tenantFilter": { + "type": "string" + }, + "count": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } } } } }, - "/RemoveUser": { - "get": { - "description": "RemoveUser", - "summary": "RemoveUser", - "tags": ["GET"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" - }, + "/api/AddAssignmentFilter": { + "post": { + "summary": "AddAssignmentFilter", + "tags": [ + "Endpoint > MEM" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "ID", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.MEM.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "displayName": { + "type": "string" + }, + "description": { + "type": "string" + }, + "assignmentFilterManagementType": { + "type": "string", + "enum": [ + "devices", + "apps" + ] + }, + "platform": { + "type": "string" + }, + "rule": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } } } } }, - "/ListTeams": { - "get": { - "description": "ListTeams", - "summary": "ListTeams", - "tags": ["GET"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "type", - "in": "query" - }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" - }, + "/api/AddAssignmentFilterTemplate": { + "post": { + "summary": "AddAssignmentFilterTemplate", + "tags": [ + "Endpoint > MEM" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "ID", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/ExecGroupsDelete": { - "get": { - "description": "ExecGroupsDelete", - "summary": "ExecGroupsDelete", - "tags": ["GET"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "GroupType", - "in": "query" - }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" - }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "id", - "in": "query" - }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "displayName", - "in": "query" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" + }, + "x-cipp-role": "Endpoint.MEM.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "GUID": { + "type": "string" + }, + "assignmentFilterManagementType": { + "type": "string", + "enum": [ + "devices", + "apps" + ] + }, + "platform": { + "type": "string" + }, + "rule": { + "type": "string" + }, + "displayname": { + "type": "string" + }, + "Description": { + "type": "string" + } } } - }, - "description": "Successful operation" + } } } } }, - "/ListRoles": { - "get": { - "description": "ListRoles", - "summary": "ListRoles", - "tags": ["GET"], - "parameters": [ + "/api/AddAutopilotConfig": { + "post": { + "summary": "AddAutopilotConfig", + "tags": [ + "Endpoint > Autopilot" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.Autopilot.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "allowWhiteGlove": { + "type": "boolean" + }, + "Assignto": { + "type": "boolean" + }, + "Autokeyboard": { + "type": "boolean" + }, + "CollectHash": { + "type": "boolean" + }, + "DeploymentMode": { + "type": "boolean" + }, + "Description": { + "type": "string" + }, + "DeviceNameTemplate": { + "type": "string" + }, + "DisplayName": { + "type": "string" + }, + "HideChangeAccount": { + "type": "boolean" + }, + "HidePrivacy": { + "type": "boolean" + }, + "HideTerms": { + "type": "boolean" + }, + "languages": { + "$ref": "#/components/schemas/LabelValue" + }, + "NotLocalAdmin": { + "type": "boolean" + }, + "selectedTenants": { + "type": "string" + } + } + } + } } } } }, - "/ListUserMailboxRules": { - "get": { - "description": "ListUserMailboxRules", - "summary": "ListUserMailboxRules", - "tags": ["GET"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" - }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "id", - "in": "query" - }, + "/api/AddBPATemplate": { + "post": { + "summary": "AddBPATemplate", + "tags": [ + "Tenant > Tools" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "UserID", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.BestPracticeAnalyser.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "style": { + "type": "string", + "enum": [ + "Tenant", + "Table" + ] + } + } + } + } } } } }, - "/ExecBECCheck": { - "get": { - "description": "ExecBECCheck", - "summary": "ExecBECCheck", - "tags": ["GET"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "tenantfilter", - "in": "query" - }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "GUID", - "in": "query" - }, + "/api/AddCAPolicy": { + "post": { + "summary": "AddCAPolicy", + "tags": [ + "Tenant > Conditional" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "userid", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.ConditionalAccess.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "CreateGroups": { + "type": "boolean" + }, + "DisableSD": { + "type": "boolean" + }, + "NewState": { + "type": "string", + "enum": [ + "donotchange", + "Enabled", + "Disabled", + "enabledForReportingButNotEnforced" + ] + }, + "overwrite": { + "type": "boolean" + }, + "RawJSON": { + "type": "string" + }, + "replacename": { + "type": "string", + "enum": [ + "leave", + "displayName", + "AllUsers" + ] + }, + "TemplateList": { + "$ref": "#/components/schemas/LabelValue" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } } } } }, - "/ListCalendarPermissions": { - "get": { - "description": "ListCalendarPermissions", - "summary": "ListCalendarPermissions", - "tags": ["GET"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "UserID", - "in": "query" - }, + "/api/AddCATemplate": { + "post": { + "summary": "AddCATemplate", + "tags": [ + "Tenant > Conditional" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "tenantfilter", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.ConditionalAccess.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "policySource": { + "$ref": "#/components/schemas/LabelValue" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } } } } }, - "/ExecAddSPN": { - "get": { - "description": "ExecAddSPN", - "summary": "ExecAddSPN", - "tags": ["GET"], - "parameters": [ + "/api/AddChocoApp": { + "post": { + "summary": "AddChocoApp", + "tags": [ + "Endpoint > Applications" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "Enable", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.Application.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ApplicationName": { + "type": "string" + }, + "AssignTo": { + "type": "string" + }, + "customArguments": { + "type": "string" + }, + "CustomGroup": { + "type": "string" + }, + "description": { + "type": "string" + }, + "DisableRestart": { + "type": "string" + }, + "InstallAsSystem": { + "type": "string" + }, + "InstallationIntent": { + "type": "string" + }, + "PackageName": { + "type": "string" + }, + "selectedTenants": { + "type": "string" + }, + "CustomRepo": { + "type": "string" + } + } + } + } } } } }, - "/ListLicenses": { - "get": { - "description": "ListLicenses", - "summary": "ListLicenses", - "tags": ["GET"], - "parameters": [ + "/api/AddConnectionFilter": { + "post": { + "summary": "AddConnectionFilter", + "tags": [ + "Email-Exchange > Transport" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.ConnectionFilter.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "PowerShellCommand": { + "type": "string" + }, + "selectedTenants": { + "type": "string" + }, + "TemplateList": { + "$ref": "#/components/schemas/LabelValue" + } + } + } + } } } } }, - "/AddCATemplate": { + "/api/AddConnectionFilterTemplate": { "post": { - "description": "AddCATemplate", - "summary": "AddCATemplate", - "tags": ["POST"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "rawjson", - "in": "body" - }, + "summary": "AddConnectionFilterTemplate", + "tags": [ + "Email-Exchange > Transport" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "name", - "in": "body" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.ConnectionFilter.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "PowerShellCommand": { + "type": "string" + } + } + } + } } } } }, - "/ExecIncidentsList": { - "get": { - "description": "ExecIncidentsList", - "summary": "ExecIncidentsList", - "tags": ["GET"], - "parameters": [ + "/api/AddContact": { + "post": { + "summary": "AddContact", + "tags": [ + "Email-Exchange > Administration > Contacts" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Contact.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Company": { + "type": "string" + }, + "Title": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "State": { + "type": "string" + }, + "tenantid": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "PostalCode": { + "type": "string" + }, + "StreetAddress": { + "type": "string" + }, + "mobilePhone": { + "type": "string" + }, + "firstName": { + "type": "string" + }, + "email": { + "type": "string" + }, + "mailTip": { + "type": "string" + }, + "website": { + "type": "string" + }, + "City": { + "type": "string" + }, + "hidefromGAL": { + "type": "string" + }, + "CountryOrRegion": { + "type": "string" + } + } + } + } } } } }, - "/AddSharedMailbox": { + "/api/AddContactTemplates": { "post": { - "description": "AddSharedMailbox", - "summary": "AddSharedMailbox", - "tags": ["POST"], - "parameters": [ + "summary": "AddContactTemplates", + "tags": [ + "Email-Exchange > Administration > Contacts" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "tenantid", - "in": "body" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Contact.ReadWrite", + "x-cipp-warnings": [ + "customDataformatter is present on the CippFormPage for this endpoint. Form field names and/or types may be renamed or restructured before POST. Verify the transformer function in the frontend page file and correct param names/types accordingly." + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "displayName": { + "type": "string" + }, + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "email": { + "type": "string" + }, + "hidefromGAL": { + "type": "boolean" + }, + "streetAddress": { + "type": "string" + }, + "postalCode": { + "type": "string" + }, + "city": { + "type": "string" + }, + "state": { + "type": "string" + }, + "country": { + "type": "string" + }, + "companyName": { + "type": "string" + }, + "mobilePhone": { + "type": "string" + }, + "businessPhone": { + "type": "string" + }, + "jobTitle": { + "type": "string" + }, + "website": { + "type": "string" + }, + "mailTip": { + "type": "string" + } + } + } + } } } } }, - "/ListApps": { - "get": { - "description": "ListApps", - "summary": "ListApps", - "tags": ["GET"], - "parameters": [ + "/api/AddDefenderDeployment": { + "post": { + "summary": "AddDefenderDeployment", + "tags": [ + "Endpoint > MEM" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } + }, + "x-cipp-role": "Endpoint.MEM.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Compliance": { + "type": "object", + "properties": { + "AllowMEMEnforceCompliance": { + "type": "boolean" + }, + "AppSync": { + "type": "boolean" + }, + "BlockunsupportedOS": { + "type": "boolean" + }, + "ConnectAndroid": { + "type": "boolean" + }, + "ConnectAndroidCompliance": { + "type": "boolean" + }, + "ConnectIos": { + "type": "boolean" + }, + "ConnectIosCompliance": { + "type": "boolean" + }, + "ConnectWindows": { + "type": "boolean" + } + } + }, + "ASR": { + "type": "object", + "properties": { + "AssignTo": { + "type": "string", + "enum": [ + "none", + "allLicensedUsers", + "AllDevices", + "AllDevicesAndUsers" + ] + }, + "BlockAdobeChild": { + "type": "boolean" + }, + "BlockCredentialStealing": { + "type": "boolean" + }, + "BlockExesMail": { + "type": "boolean" + }, + "blockJSVB": { + "type": "boolean" + }, + "BlockObfuscatedScripts": { + "type": "boolean" + }, + "BlockOfficeApps": { + "type": "boolean" + }, + "blockOfficeChild": { + "type": "boolean" + }, + "blockOfficeComChild": { + "type": "boolean" + }, + "BlockOfficeExes": { + "type": "boolean" + }, + "BlockPSExec": { + "type": "boolean" + }, + "BlockSafeMode": { + "type": "boolean" + }, + "BlockSystemTools": { + "type": "boolean" + }, + "BlockUnsignedDrivers": { + "type": "boolean" + }, + "BlockUntrustedUSB": { + "type": "boolean" + }, + "BlockWebshellForServers": { + "type": "boolean" + }, + "BlockWin32Macro": { + "type": "boolean" + }, + "BlockYoungExe": { + "type": "boolean" + }, + "EnableRansomwareVac": { + "type": "boolean" + }, + "Mode": { + "type": "string", + "enum": [ + "block", + "audit", + "warn" + ] + }, + "WMIPersistence": { + "type": "boolean" + } + } + }, + "EDR": { + "type": "object", + "properties": { + "AssignTo": { + "type": "string", + "enum": [ + "none", + "allLicensedUsers", + "AllDevices", + "AllDevicesAndUsers" + ] + }, + "Config": { + "type": "boolean" + }, + "SampleSharing": { + "type": "boolean" + } + } + }, + "Exclusion": { + "type": "object", + "properties": { + "AssignTo": { + "type": "string", + "enum": [ + "none", + "allLicensedUsers", + "AllDevices", + "AllDevicesAndUsers" + ] + } + } + }, + "Policy": { + "type": "object", + "properties": { + "AllowBehavior": { + "type": "boolean" + }, + "AllowCloudProtection": { + "type": "boolean" + }, + "AllowDownloadable": { + "type": "boolean" + }, + "AllowEmailScanning": { + "type": "boolean" + }, + "AllowFullScanNetwork": { + "type": "boolean" + }, + "AllowFullScanRemovable": { + "type": "boolean" + }, + "AllowNetwork": { + "type": "boolean" + }, + "AllowOnAccessProtection": { + "$ref": "#/components/schemas/LabelValue" + }, + "AllowRealTime": { + "type": "boolean" + }, + "AllowScriptScan": { + "type": "boolean" + }, + "AllowUI": { + "type": "boolean" + }, + "AssignTo": { + "type": "string", + "enum": [ + "none", + "allLicensedUsers", + "AllDevices", + "AllDevicesAndUsers" + ] + }, + "AvgCPULoadFactor": { + "type": "number" + }, + "CheckSigs": { + "type": "boolean" + }, + "CloudBlockLevel": { + "$ref": "#/components/schemas/LabelValue" + }, + "CloudExtendedTimeout": { + "type": "number" + }, + "DisableCatchupFullScan": { + "type": "boolean" + }, + "DisableCatchupQuickScan": { + "type": "boolean" + }, + "DisableLocalAdminMerge": { + "type": "boolean" + }, + "EnableNetworkProtection": { + "$ref": "#/components/schemas/LabelValue" + }, + "LowCPU": { + "type": "boolean" + }, + "MeteredConnectionUpdates": { + "type": "boolean" + }, + "Remediation.High": { + "$ref": "#/components/schemas/LabelValue" + }, + "Remediation.Low": { + "$ref": "#/components/schemas/LabelValue" + }, + "Remediation.Moderate": { + "$ref": "#/components/schemas/LabelValue" + }, + "Remediation.Severe": { + "$ref": "#/components/schemas/LabelValue" + }, + "ScanArchives": { + "type": "boolean" + }, + "SignatureUpdateInterval": { + "type": "number" + }, + "SubmitSamplesConsent": { + "$ref": "#/components/schemas/LabelValue" + } + } + }, + "selectedTenants": { + "type": "string" + }, + "showASR": { + "type": "boolean" + }, + "showDefenderDefaults": { + "type": "boolean" + }, + "showDefenderSetup": { + "type": "boolean" + }, + "showExclusionPolicy": { + "type": "boolean" + }, + "appSync": { + "type": "string" + }, + "ConnectIos": { + "type": "string" + }, + "ConnectAndroid": { + "type": "string" + }, + "ConnectIosCompliance": { + "type": "string" + }, + "BlockunsupportedOS": { + "type": "string" + }, + "ConnectMac": { + "type": "string" + }, + "Connectwindows": { + "type": "string" + }, + "ConnectAndroidCompliance": { + "type": "string" + }, + "AssignTo": { + "type": "string" + }, + "excludedExtensions": { + "type": "string" + }, + "excludedProcesses": { + "type": "string" + }, + "excludedPaths": { + "type": "string" + }, + "Mode": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/AddDomain": { + "post": { + "summary": "AddDomain", + "tags": [ + "Tenant > Administration > Domains" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Administration.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "domain": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/AddEditTransportRule": { + "post": { + "summary": "AddEditTransportRule", + "tags": [ + "Email-Exchange > Transport" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "This function creates a new transport rule or edits an existing one (mail flow rule).\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", + "x-cipp-role": "Exchange.TransportRule.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "AttachmentHasExecutableContent": { + "type": "string" + }, + "AttachmentIsPasswordProtected": { + "type": "string" + }, + "AttachmentIsUnsupported": { + "type": "string" + }, + "AttachmentNameMatchesPatterns": { + "type": "string" + }, + "AttachmentProcessingLimitExceeded": { + "type": "string" + }, + "AttachmentPropertyContainsWords": { + "type": "string" + }, + "ExceptIfAttachmentHasExecutableContent": { + "type": "string" + }, + "ExceptIfAttachmentIsPasswordProtected": { + "type": "string" + }, + "ExceptIfAttachmentIsUnsupported": { + "type": "string" + }, + "ExceptIfAttachmentNameMatchesPatterns": { + "type": "string" + }, + "ExceptIfAttachmentProcessingLimitExceeded": { + "type": "string" + }, + "ExceptIfAttachmentPropertyContainsWords": { + "type": "string" + }, + "IncidentReportContent": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LabelValue" + } + }, + "actionType": { + "type": "array", + "items": { + "type": "string" + } + }, + "ActivationDate": { + "type": "string", + "format": "date-time" + }, + "AnyOfCcHeader": { + "type": "string" + }, + "AnyOfCcHeaderMemberOf": { + "type": "string" + }, + "AnyOfRecipientAddressContainsWords": { + "type": "string" + }, + "AnyOfRecipientAddressMatchesPatterns": { + "type": "string" + }, + "AnyOfToCcHeader": { + "type": "string" + }, + "AnyOfToCcHeaderMemberOf": { + "type": "string" + }, + "AnyOfToHeader": { + "type": "string" + }, + "AnyOfToHeaderMemberOf": { + "type": "string" + }, + "ApplyClassification": { + "type": "string" + }, + "ApplyHtmlDisclaimerFallbackAction": { + "$ref": "#/components/schemas/LabelValue" + }, + "ApplyHtmlDisclaimerLocation": { + "$ref": "#/components/schemas/LabelValue" + }, + "ApplyHtmlDisclaimerText": { + "type": "string" + }, + "ApplyOME": { + "type": "string" + }, + "applyToAllMessages": { + "type": "boolean" + }, + "AttachmentContainsWords": { + "type": "string" + }, + "AttachmentExtensionMatchesWords": { + "type": "string" + }, + "AttachmentMatchesPatterns": { + "type": "string" + }, + "AttachmentSizeOver": { + "type": "string" + }, + "BlindCopyTo": { + "type": "string" + }, + "Comments": { + "type": "string" + }, + "conditionType": { + "type": "array", + "items": { + "type": "string" + } + }, + "CopyTo": { + "type": "string" + }, + "DeleteMessage": { + "type": "string" + }, + "Enabled": { + "type": "boolean" + }, + "ExceptIfAnyOfCcHeader": { + "type": "string" + }, + "ExceptIfAnyOfCcHeaderMemberOf": { + "type": "string" + }, + "ExceptIfAnyOfRecipientAddressContainsWords": { + "type": "string" + }, + "ExceptIfAnyOfRecipientAddressMatchesPatterns": { + "type": "string" + }, + "ExceptIfAnyOfToCcHeader": { + "type": "string" + }, + "ExceptIfAnyOfToCcHeaderMemberOf": { + "type": "string" + }, + "ExceptIfAnyOfToHeader": { + "type": "string" + }, + "ExceptIfAnyOfToHeaderMemberOf": { + "type": "string" + }, + "ExceptIfAttachmentContainsWords": { + "type": "string" + }, + "ExceptIfAttachmentExtensionMatchesWords": { + "type": "string" + }, + "ExceptIfAttachmentMatchesPatterns": { + "type": "string" + }, + "ExceptIfAttachmentSizeOver": { + "type": "string" + }, + "ExceptIfFrom": { + "type": "string" + }, + "ExceptIfFromAddressContainsWords": { + "type": "string" + }, + "ExceptIfFromAddressMatchesPatterns": { + "type": "string" + }, + "ExceptIfFromMemberOf": { + "type": "string" + }, + "ExceptIfFromScope": { + "type": "string" + }, + "ExceptIfHeaderContainsWords": { + "type": "string" + }, + "ExceptIfHeaderContainsWordsMessageHeader": { + "type": "string" + }, + "ExceptIfHeaderMatchesPatterns": { + "type": "string" + }, + "ExceptIfHeaderMatchesPatternsMessageHeader": { + "type": "string" + }, + "ExceptIfMessageSizeOver": { + "type": "string" + }, + "ExceptIfMessageTypeMatches": { + "type": "string" + }, + "ExceptIfRecipientAddressContainsWords": { + "type": "string" + }, + "ExceptIfRecipientAddressMatchesPatterns": { + "type": "string" + }, + "ExceptIfRecipientDomainIs": { + "type": "string" + }, + "ExceptIfSCLOver": { + "type": "string" + }, + "ExceptIfSenderDomainIs": { + "type": "string" + }, + "ExceptIfSenderIpRanges": { + "type": "string" + }, + "ExceptIfSentTo": { + "type": "string" + }, + "ExceptIfSentToMemberOf": { + "type": "string" + }, + "ExceptIfSentToScope": { + "type": "string" + }, + "ExceptIfSubjectContainsWords": { + "type": "string" + }, + "ExceptIfSubjectMatchesPatterns": { + "type": "string" + }, + "ExceptIfSubjectOrBodyContainsWords": { + "type": "string" + }, + "ExceptIfSubjectOrBodyMatchesPatterns": { + "type": "string" + }, + "ExceptIfWithImportance": { + "type": "string" + }, + "exceptionType": { + "type": "array", + "items": { + "type": "string" + } + }, + "ExpiryDate": { + "type": "string", + "format": "date-time" + }, + "From": { + "type": "string" + }, + "FromAddressContainsWords": { + "type": "string" + }, + "FromAddressMatchesPatterns": { + "type": "string" + }, + "FromMemberOf": { + "type": "string" + }, + "FromScope": { + "type": "string" + }, + "GenerateIncidentReport": { + "type": "string" + }, + "GenerateNotification": { + "type": "string" + }, + "HeaderContainsWords": { + "type": "string" + }, + "HeaderContainsWordsMessageHeader": { + "type": "string" + }, + "HeaderMatchesPatterns": { + "type": "string" + }, + "HeaderMatchesPatternsMessageHeader": { + "type": "string" + }, + "MessageSizeOver": { + "type": "string" + }, + "MessageTypeMatches": { + "type": "string" + }, + "Mode": { + "$ref": "#/components/schemas/LabelValue" + }, + "ModerateMessageByManager": { + "type": "string" + }, + "ModerateMessageByUser": { + "type": "string" + }, + "Name": { + "type": "string" + }, + "PrependSubject": { + "type": "string" + }, + "Priority": { + "type": "number" + }, + "Quarantine": { + "type": "string" + }, + "RecipientAddressContainsWords": { + "type": "string" + }, + "RecipientAddressMatchesPatterns": { + "type": "string" + }, + "RecipientDomainIs": { + "type": "string" + }, + "RedirectMessageTo": { + "type": "string" + }, + "RejectMessageEnhancedStatusCode": { + "type": "string" + }, + "RejectMessageReasonText": { + "type": "string" + }, + "RemoveHeader": { + "type": "string" + }, + "RouteMessageOutboundConnector": { + "type": "string" + }, + "ruleId": { + "type": "string" + }, + "SCLOver": { + "type": "string" + }, + "SenderAddressLocation": { + "$ref": "#/components/schemas/LabelValue" + }, + "SenderDomainIs": { + "type": "string" + }, + "SenderIpRanges": { + "type": "string" + }, + "SentTo": { + "type": "string" + }, + "SentToMemberOf": { + "type": "string" + }, + "SentToScope": { + "type": "string" + }, + "SetAuditSeverity": { + "$ref": "#/components/schemas/LabelValue" + }, + "SetHeaderName": { + "type": "string" + }, + "SetHeaderValue": { + "type": "string" + }, + "SetSCL": { + "type": "string" + }, + "State": { + "type": "string" + }, + "StopRuleProcessing": { + "type": "boolean" + }, + "SubjectContainsWords": { + "type": "string" + }, + "SubjectMatchesPatterns": { + "type": "string" + }, + "SubjectOrBodyContainsWords": { + "type": "string" + }, + "SubjectOrBodyMatchesPatterns": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "WithImportance": { + "type": "string" + }, + "value": { + "type": "string" + }, + "Count": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/AddEnrollment": { + "post": { + "summary": "AddEnrollment", + "tags": [ + "Endpoint > Autopilot" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.Autopilot.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "AllowFail": { + "type": "boolean" + }, + "AllowReset": { + "type": "boolean" + }, + "blockDevice": { + "type": "boolean" + }, + "EnableLog": { + "type": "boolean" + }, + "ErrorMessage": { + "type": "string" + }, + "InstallWindowsUpdates": { + "type": "boolean" + }, + "OBEEOnly": { + "type": "boolean" + }, + "selectedTenants": { + "type": "string" + }, + "ShowProgress": { + "type": "boolean" + }, + "TimeOutInMinutes": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/AddEquipmentMailbox": { + "post": { + "summary": "AddEquipmentMailbox", + "tags": [ + "Email-Exchange > Resources" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Equipment.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "displayName": { + "type": "string" + }, + "domain": { + "$ref": "#/components/schemas/LabelValue" + }, + "tenantID": { + "type": "string" + }, + "username": { + "type": "string" + }, + "userPrincipalName": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/AddExConnector": { + "post": { + "summary": "AddExConnector", + "tags": [ + "Email-Exchange > Transport" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Connector.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "PowerShellCommand": { + "type": "string" + }, + "selectedTenants": { + "type": "string" + }, + "TemplateList": { + "$ref": "#/components/schemas/LabelValue" + }, + "comment": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/AddExConnectorTemplate": { + "post": { + "summary": "AddExConnectorTemplate", + "tags": [ + "Email-Exchange > Transport" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Connector.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "cippconnectortype": { + "type": "string" + }, + "name": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/AddGroup": { + "post": { + "summary": "AddGroup", + "tags": [ + "Identity > Administration > Groups" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Identity.Group.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "membershipRules": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "description": { + "type": "string" + }, + "username": { + "type": "string" + }, + "groupType": { + "type": "string", + "enum": [ + "azurerole", + "generic", + "m365", + "dynamic", + "dynamicdistribution", + "distribution", + "security" + ] + }, + "allowExternal": { + "type": "boolean" + }, + "subscribeMembers": { + "type": "boolean" + }, + "primDomain": { + "$ref": "#/components/schemas/LabelValue" + }, + "owners": { + "type": "array", + "items": { + "type": "string" + } + }, + "members": { + "type": "array", + "items": { + "type": "string" + } + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/AddGroupTeam": { + "post": { + "summary": "AddGroupTeam", + "tags": [ + "Identity > Administration > Groups" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Identity.Group.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "GroupId": { + "type": "string" + }, + "TeamSettings": { + "type": "string" + }, + "TenantFilter": { + "type": "string" + } + }, + "required": [ + "TenantFilter" + ] + } + } + } + } + } + }, + "/api/AddGroupTemplate": { + "post": { + "summary": "AddGroupTemplate", + "tags": [ + "Identity > Administration > Groups" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Identity.Group.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "GUID": { + "type": "string" + }, + "username": { + "type": "string" + }, + "groupType": { + "type": "string", + "enum": [ + "azurerole", + "generic", + "m365", + "dynamic", + "dynamicDistribution", + "distribution", + "security" + ] + }, + "allowExternal": { + "type": "boolean" + }, + "subscribeMembers": { + "type": "boolean" + }, + "membershipRules": { + "type": "string" + }, + "displayname": { + "type": "string" + }, + "Description": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/AddGuest": { + "post": { + "summary": "AddGuest", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Identity.User.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "addrow": { + "type": "object", + "properties": { + "displayName": { + "type": "string" + }, + "mail": { + "type": "string" + }, + "redirectUri": { + "type": "string" + } + } + }, + "bulkGuests": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "mail": { + "type": "string" + }, + "message": { + "type": "string" + }, + "redirectUri": { + "type": "string" + }, + "sendInvite": { + "type": "boolean" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/AddIntuneReusableSetting": { + "post": { + "summary": "AddIntuneReusableSetting", + "tags": [ + "Endpoint > MEM" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.MEM.ReadWrite", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "TemplateId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tenantFilter": { + "type": "string" + }, + "ID": { + "type": "string" + }, + "TemplateId": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "description": { + "type": "string" + }, + "rawJSON": { + "type": "string" + }, + "TemplateList": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/AddIntuneReusableSettingTemplate": { + "post": { + "summary": "AddIntuneReusableSettingTemplate", + "tags": [ + "Endpoint > MEM" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.MEM.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "GUID": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "description": { + "type": "string" + }, + "rawJSON": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "package": { + "type": "string" + }, + "DisplayName": { + "type": "string" + }, + "displayname": { + "type": "string" + }, + "Description": { + "type": "string" + }, + "RawJSON": { + "type": "string" + }, + "json": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/AddIntuneTemplate": { + "post": { + "summary": "AddIntuneTemplate", + "tags": [ + "Endpoint > MEM" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.MEM.ReadWrite", + "parameters": [ + { + "name": "ID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "ODataType", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "URLName", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "ID": { + "type": "string" + }, + "ODataType": { + "type": "string" + }, + "policySource": { + "$ref": "#/components/schemas/LabelValue" + }, + "RawJSON": { + "type": "string" + }, + "TemplateType": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "URLName": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/AddJITAdminTemplate": { + "post": { + "summary": "AddJITAdminTemplate", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Identity.Role.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "defaultDomain": { + "$ref": "#/components/schemas/LabelValue" + }, + "defaultDuration": { + "$ref": "#/components/schemas/LabelValue" + }, + "defaultExistingUser": { + "$ref": "#/components/schemas/LabelValue" + }, + "defaultExpireAction": { + "$ref": "#/components/schemas/LabelValue" + }, + "defaultFirstName": { + "type": "string" + }, + "defaultForTenant": { + "type": "boolean" + }, + "defaultLastName": { + "type": "string" + }, + "defaultNotificationActions": { + "type": "array", + "items": { + "type": "string" + } + }, + "defaultRoles": { + "$ref": "#/components/schemas/LabelValue" + }, + "defaultUserAction": { + "type": "string" + }, + "defaultUserName": { + "type": "string" + }, + "generateTAPByDefault": { + "type": "boolean" + }, + "reasonTemplate": { + "type": "string" + }, + "templateName": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/AddMSPApp": { + "post": { + "summary": "AddMSPApp", + "tags": [ + "Endpoint > Applications" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.Application.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "AssignTo": { + "type": "string" + }, + "CustomGroup": { + "type": "string" + }, + "DisplayName": { + "type": "string" + }, + "PackageName": { + "type": "string" + }, + "params": { + "type": "string" + }, + "RMMName": { + "$ref": "#/components/schemas/LabelValue", + "type": "object", + "properties": { + "value": { + "type": "string" + } + } + }, + "selectedTenants": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/AddNamedLocation": { + "post": { + "summary": "AddNamedLocation", + "tags": [ + "Tenant > Conditional" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.ConditionalAccess.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Countries": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LabelValue" + } + }, + "includeUnknownCountriesAndRegions": { + "type": "boolean" + }, + "Ips": { + "type": "string" + }, + "policyName": { + "type": "string" + }, + "selectedTenants": { + "type": "string" + }, + "Trusted": { + "type": "boolean" + }, + "Type": { + "type": "string", + "enum": [ + "Countries", + "IPLocation" + ] + } + } + } + } + } + } + } + }, + "/api/AddOfficeApp": { + "post": { + "summary": "AddOfficeApp", + "tags": [ + "Endpoint > Applications" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.Application.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "AcceptLicense": { + "type": "string" + }, + "arch": { + "type": "string" + }, + "AssignTo": { + "type": "string" + }, + "CustomGroup": { + "type": "string" + }, + "customXml": { + "type": "string" + }, + "excludedApps": { + "type": "string" + }, + "languages": { + "type": "string" + }, + "RemoveVersions": { + "type": "string" + }, + "selectedTenants": { + "type": "string" + }, + "SharedComputerActivation": { + "type": "string" + }, + "updateChannel": { + "type": "string" + }, + "useCustomXml": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/AddPolicy": { + "post": { + "summary": "AddPolicy", + "tags": [ + "Endpoint > MEM" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.MEM.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tenantFilter": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "Description": { + "type": "string" + }, + "AssignTo": { + "type": "string" + }, + "excludeGroup": { + "type": "string" + }, + "AssignmentFilterName": { + "type": "string" + }, + "assignmentFilter": { + "type": "string" + }, + "assignmentFilterType": { + "type": "string" + }, + "customGroup": { + "type": "string" + }, + "RAWJson": { + "type": "string" + }, + "replacemap": { + "type": "string" + }, + "reusableSettings": { + "type": "string" + }, + "TemplateID": { + "type": "string" + }, + "TemplateGUID": { + "type": "string" + }, + "TemplateType": { + "type": "string" + }, + "Count": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/AddQuarantinePolicy": { + "post": { + "summary": "AddQuarantinePolicy", + "tags": [ + "Email-Exchange > Spamfilter" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Spamfilter.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "AllowSender": { + "type": "boolean" + }, + "BlockSender": { + "type": "boolean" + }, + "Delete": { + "type": "boolean" + }, + "IncludeMessagesFromBlockedSenderAddress": { + "type": "boolean" + }, + "Name": { + "type": "string" + }, + "Preview": { + "type": "boolean" + }, + "QuarantineNotification": { + "type": "boolean" + }, + "ReleaseActionPreference": { + "$ref": "#/components/schemas/LabelValue" + }, + "selectedTenants": { + "type": "string" + }, + "TemplateList": { + "$ref": "#/components/schemas/LabelValue" + } + } + } + } + } + } + } + }, + "/api/AddRoomList": { + "post": { + "summary": "AddRoomList", + "tags": [ + "Email-Exchange > Resources" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Room.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "displayName": { + "type": "string" + }, + "primDomain": { + "$ref": "#/components/schemas/LabelValue" + }, + "tenantFilter": { + "type": "string" + }, + "tenantid": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/AddRoomMailbox": { + "post": { + "summary": "AddRoomMailbox", + "tags": [ + "Email-Exchange > Resources" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Room.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "domain": { + "$ref": "#/components/schemas/LabelValue" + }, + "ResourceCapacity": { + "type": "string" + }, + "tenantid": { + "type": "string" + }, + "username": { + "type": "string" + }, + "userPrincipalName": { + "type": "string" + }, + "DisplayName": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/AddSafeLinksPolicyFromTemplate": { + "post": { + "summary": "AddSafeLinksPolicyFromTemplate", + "tags": [ + "Security > Safe-Links-Policy" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "This function deploys SafeLinks policies and rules from templates to selected tenants.\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", + "x-cipp-role": "Exchange.SafeLinks.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "selectedTenants": { + "type": "string" + }, + "TemplateList": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "/api/AddSafeLinksPolicyTemplate": { + "post": { + "summary": "AddSafeLinksPolicyTemplate", + "tags": [ + "Security > Safe-Links-Policy" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.SafeLinks.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "AdminDisplayName": { + "type": "string" + }, + "Description": { + "type": "string" + }, + "Name": { + "type": "string" + }, + "PolicyName": { + "type": "string" + }, + "TemplateDescription": { + "type": "string" + }, + "TemplateName": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/AddScheduledItem": { + "post": { + "summary": "AddScheduledItem", + "tags": [ + "CIPP > Scheduler" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Scheduler.ReadWrite", + "x-cipp-warnings": [ + "This endpoint does not use CippFormPage's customDataformatter — CippSchedulerForm calls ApiPostCall directly. The raw form values (with empty fields stripped) are posted as-is." + ], + "parameters": [ + { + "name": "hidden", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "DisallowDuplicateName", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "advancedParameters": { + "type": "boolean" + }, + "antiphishing": { + "type": "boolean" + }, + "antispam": { + "type": "boolean" + }, + "backup": { + "$ref": "#/components/schemas/LabelValue" + }, + "ca": { + "type": "boolean" + }, + "CippCustomVariables": { + "type": "boolean" + }, + "CippScriptedAlerts": { + "type": "boolean" + }, + "CippWebhookAlerts": { + "type": "boolean" + }, + "email": { + "type": "boolean" + }, + "groups": { + "type": "boolean" + }, + "intunecompliance": { + "type": "boolean" + }, + "intuneconfig": { + "type": "boolean" + }, + "intuneprotection": { + "type": "boolean" + }, + "overwrite": { + "type": "boolean" + }, + "psa": { + "type": "boolean" + }, + "Trigger": { + "type": "object" + }, + "users": { + "type": "boolean" + }, + "webhook": { + "type": "boolean" + }, + "tenantFilter": { + "type": "object" + }, + "Name": { + "type": "string" + }, + "command": { + "type": "object" + }, + "taskType": { + "type": "object" + }, + "ScheduledTime": { + "type": "integer" + }, + "Recurrence": { + "type": "object" + }, + "parameters": { + "type": "object" + }, + "RawJsonParameters": { + "type": "string" + }, + "postExecution": { + "type": "array", + "items": { + "type": "string" + } + }, + "reference": { + "type": "string" + }, + "RowKey": { + "type": "string" + }, + "RunNow": { + "type": "boolean" + }, + "DesiredStartTime": { + "type": "string" + }, + "DisallowDuplicateName": { + "type": "boolean" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/AddSharedMailbox": { + "post": { + "summary": "AddSharedMailbox", + "tags": [ + "Email-Exchange > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tenantID": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "username": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "addedAliases": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/AddSite": { + "post": { + "summary": "AddSite", + "tags": [ + "Teams-Sharepoint" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Sharepoint.Site.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sensitivityLabel": { + "type": "string" + }, + "siteDescription": { + "type": "string" + }, + "siteDesign": { + "$ref": "#/components/schemas/LabelValue", + "type": "object", + "properties": { + "value": { + "type": "string" + } + } + }, + "siteName": { + "type": "string" + }, + "siteOwner": { + "$ref": "#/components/schemas/LabelValue", + "type": "object", + "properties": { + "value": { + "type": "string" + } + } + }, + "templateName": { + "$ref": "#/components/schemas/LabelValue", + "type": "object", + "properties": { + "value": { + "type": "string" + } + } + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/AddSiteBulk": { + "post": { + "summary": "AddSiteBulk", + "tags": [ + "Teams-Sharepoint" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Sharepoint.Site.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "bulkSites": { + "type": "string" + }, + "sensitivityLabel": { + "type": "string" + }, + "siteDescription": { + "type": "string" + }, + "siteDesign": { + "type": "string" + }, + "siteName": { + "type": "string" + }, + "siteOwner": { + "type": "string" + }, + "templateName": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/AddSpamFilter": { + "post": { + "summary": "AddSpamFilter", + "tags": [ + "Email-Exchange > Spamfilter" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.SpamFilter.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "PowerShellCommand": { + "type": "string" + }, + "Priority": { + "type": "string" + }, + "selectedTenants": { + "type": "string" + }, + "TemplateList": { + "$ref": "#/components/schemas/LabelValue" + }, + "name": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/AddSpamFilterTemplate": { + "post": { + "summary": "AddSpamFilterTemplate", + "tags": [ + "Email-Exchange > Spamfilter" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Spamfilter.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "PowerShellCommand": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/AddStandardsDeploy": { + "post": { + "summary": "AddStandardsDeploy", + "tags": [ + "Tenant > Standards" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Standards.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tenant": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/AddStandardsTemplate": { + "post": { + "summary": "AddStandardsTemplate", + "tags": [ + "Tenant > Standards" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Standards.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "GUID": { + "type": "string" + }, + "templateName": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/AddStoreApp": { + "post": { + "summary": "AddStoreApp", + "tags": [ + "Endpoint > Applications" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.Application.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ApplicationName": { + "type": "string" + }, + "AssignTo": { + "type": "string" + }, + "CustomGroup": { + "type": "string" + }, + "description": { + "type": "string" + }, + "InstallationIntent": { + "type": "string" + }, + "PackageName": { + "type": "string" + }, + "selectedTenants": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/AddTeam": { + "post": { + "summary": "AddTeam", + "tags": [ + "Teams-Sharepoint" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Teams.Group.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "displayName": { + "type": "string" + }, + "description": { + "type": "string" + }, + "owner": { + "type": "string" + }, + "visibility": { + "type": "string" + }, + "tenantid": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/AddTenant": { + "post": { + "summary": "AddTenant", + "tags": [ + "Tenant > Administration > Tenant" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Config.ReadWrite", + "parameters": [ + { + "name": "Action", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "TenantName", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Action": { + "type": "string" + }, + "AddressLine1": { + "type": "string" + }, + "AddressLine2": { + "type": "string" + }, + "City": { + "type": "string" + }, + "CompanyName": { + "type": "string" + }, + "Country": { + "type": "string" + }, + "Email": { + "type": "string" + }, + "FirstName": { + "type": "string" + }, + "LastName": { + "type": "string" + }, + "PhoneNumber": { + "type": "string" + }, + "PostalCode": { + "type": "string" + }, + "State": { + "type": "string" + }, + "TenantName": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/AddTenantAllowBlockList": { + "post": { + "summary": "AddTenantAllowBlockList", + "tags": [ + "Email-Exchange > Spamfilter" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.SpamFilter.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "entries": { + "type": "string" + }, + "listMethod": { + "$ref": "#/components/schemas/LabelValue" + }, + "listType": { + "$ref": "#/components/schemas/LabelValue" + }, + "NoExpiration": { + "type": "boolean" + }, + "notes": { + "type": "string" + }, + "RemoveAfter": { + "type": "boolean" + }, + "tenantID": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/AddTestReport": { + "post": { + "summary": "AddTestReport", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Dashboard.Read", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "DevicesTests": { + "type": "string" + }, + "IdentityTests": { + "type": "string" + }, + "name": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/AddTransportRule": { + "post": { + "summary": "AddTransportRule", + "tags": [ + "Email-Exchange > Transport" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.TransportRule.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "PowerShellCommand": { + "type": "string" + }, + "selectedTenants": { + "type": "string" + }, + "TemplateList": { + "$ref": "#/components/schemas/LabelValue" + }, + "PSObject": { + "type": "string" + }, + "name": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/AddTransportTemplate": { + "post": { + "summary": "AddTransportTemplate", + "tags": [ + "Email-Exchange > Transport" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.TransportRule.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "PowerShellCommand": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/AddUser": { + "post": { + "summary": "AddUser", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Create a new user in a tenant, optionally scheduled", + "x-cipp-role": "Identity.User.ReadWrite", + "x-cipp-scheduled-branch": true, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tenantFilter": { + "type": "string" + }, + "givenName": { + "type": "string" + }, + "surname": { + "type": "string" + }, + "username": { + "type": "string" + }, + "password": { + "type": "string" + }, + "MustChangePass": { + "type": "boolean" + }, + "usageLocation": { + "$ref": "#/components/schemas/LabelValue" + }, + "licenses": { + "type": "array", + "items": { + "type": "string" + } + }, + "jobTitle": { + "type": "string" + }, + "department": { + "type": "string" + }, + "streetAddress": { + "type": "string" + }, + "city": { + "type": "string" + }, + "state": { + "type": "string" + }, + "postalCode": { + "type": "string" + }, + "country": { + "type": "string" + }, + "companyName": { + "type": "string" + }, + "mobilePhone": { + "type": "string" + }, + "businessPhones": { + "type": "array", + "items": { + "type": "string" + } + }, + "copyFrom": { + "$ref": "#/components/schemas/LabelValue" + }, + "userTemplate": { + "$ref": "#/components/schemas/LabelValue" + }, + "Scheduled": { + "$ref": "#/components/schemas/ScheduledTask", + "type": "object", + "properties": { + "date": { + "type": "string" + }, + "Enabled": { + "type": "string" + } + } + }, + "reference": { + "type": "string" + }, + "PostExecution": { + "type": "string" + }, + "mailNickname": { + "type": "string" + }, + "PrimDomain": { + "type": "object", + "properties": { + "value": { + "type": "string" + } + } + }, + "DisplayName": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/AddUserBulk": { + "post": { + "summary": "Bulk-create users in a tenant via CSV import", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Identity.User.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tenantFilter": { + "type": "string" + }, + "BulkUser": { + "type": "array", + "items": { + "type": "string" + } + }, + "licenses": { + "type": "array", + "items": { + "type": "string" + } + }, + "usageLocation": { + "$ref": "#/components/schemas/LabelValue" + }, + "value": { + "type": "string" + }, + "label": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/AddUserDefaults": { + "post": { + "summary": "AddUserDefaults", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Identity.User.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "addedAliases": { + "type": "string", + "description": "Additional SMTP aliases, newline-delimited (server splits on \\n)" + }, + "city": { + "type": "string" + }, + "companyName": { + "type": "string" + }, + "copyFrom": { + "$ref": "#/components/schemas/LabelValue" + }, + "country": { + "type": "string" + }, + "defaultForTenant": { + "type": "string" + }, + "department": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "givenName": { + "type": "string" + }, + "GUID": { + "type": "string" + }, + "jobTitle": { + "type": "string" + }, + "licenses": { + "type": "array", + "items": { + "type": "string" + }, + "description": "License SKU IDs to assign" + }, + "mobilePhone": { + "type": "string" + }, + "MustChangePass": { + "type": "boolean" + }, + "otherMails": { + "type": "array", + "items": { + "type": "string" + } + }, + "password": { + "type": "string" + }, + "postalCode": { + "type": "string" + }, + "primDomain": { + "$ref": "#/components/schemas/LabelValue" + }, + "removeLicenses": { + "type": "boolean" + }, + "setManager": { + "$ref": "#/components/schemas/LabelValue" + }, + "setSponsor": { + "$ref": "#/components/schemas/LabelValue" + }, + "state": { + "type": "string" + }, + "streetAddress": { + "type": "string" + }, + "surname": { + "type": "string" + }, + "templateName": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "usageLocation": { + "$ref": "#/components/schemas/LabelValue" + }, + "usernameFormat": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/AddWin32ScriptApp": { + "post": { + "summary": "AddWin32ScriptApp", + "tags": [ + "Endpoint > Applications" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.Application.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "AssignTo": { + "type": "string" + }, + "CustomGroup": { + "type": "string" + }, + "description": { + "type": "string" + }, + "detectionFile": { + "type": "string" + }, + "detectionPath": { + "type": "string" + }, + "DisableRestart": { + "type": "string" + }, + "enforceSignatureCheck": { + "type": "string" + }, + "InstallAsSystem": { + "type": "string" + }, + "InstallationIntent": { + "type": "string" + }, + "installScript": { + "type": "string" + }, + "publisher": { + "type": "string" + }, + "runAs32Bit": { + "type": "string" + }, + "selectedTenants": { + "type": "string" + }, + "uninstallScript": { + "type": "string" + }, + "applicationName": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/BestPracticeAnalyser_List": { + "get": { + "summary": "BestPracticeAnalyser_List", + "tags": [ + "Tenant > Standards" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.BestPracticeAnalyser.Read" + } + }, + "/api/CIPPDBTestsRun": { + "get": { + "summary": "CIPPDBTestsRun", + "tags": [ + "Activity Triggers > Tests" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Tests.Read" + } + }, + "/api/CIPPOffboardingJob": { + "get": { + "summary": "CIPPOffboardingJob", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + } + } + }, + "/api/CIPPStandardsRun": { + "get": { + "summary": "CIPPStandardsRun", + "tags": [ + "Tenant > Standards" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Standards.ReadWrite" + } + }, + "/api/CreateSafeLinksPolicyTemplate": { + "post": { + "summary": "CreateSafeLinksPolicyTemplate", + "tags": [ + "Security > Safe-Links-Policy" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "This function creates a new Safe Links policy template from scratch.\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", + "x-cipp-role": "Exchange.SafeLinks.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "TemplateName": { + "type": "string" + }, + "TemplateDescription": { + "type": "string" + }, + "PolicyName": { + "type": "string" + }, + "AdminDisplayName": { + "type": "string" + }, + "EnableSafeLinksForEmail": { + "type": "boolean" + }, + "EnableSafeLinksForTeams": { + "type": "boolean" + }, + "EnableSafeLinksForOffice": { + "type": "boolean" + }, + "TrackClicks": { + "type": "boolean" + }, + "AllowClickThrough": { + "type": "boolean" + }, + "ScanUrls": { + "type": "boolean" + }, + "EnableForInternalSenders": { + "type": "boolean" + }, + "DeliverMessageAfterScan": { + "type": "boolean" + }, + "DisableUrlRewrite": { + "type": "boolean" + }, + "EnableOrganizationBranding": { + "type": "boolean" + }, + "DoNotRewriteUrls": { + "type": "array", + "items": { + "type": "string" + } + }, + "CustomNotificationText": { + "type": "string" + }, + "RuleName": { + "type": "string" + }, + "Priority": { + "type": "integer" + }, + "State": { + "type": "boolean" + }, + "Comments": { + "type": "string" + }, + "SentTo": { + "type": "array", + "items": { + "type": "string" + } + }, + "SentToMemberOf": { + "type": "array", + "items": { + "type": "string" + } + }, + "RecipientDomainIs": { + "type": "array", + "items": { + "type": "string" + } + }, + "ExceptIfSentTo": { + "type": "array", + "items": { + "type": "string" + } + }, + "ExceptIfSentToMemberOf": { + "type": "array", + "items": { + "type": "string" + } + }, + "ExceptIfRecipientDomainIs": { + "type": "array", + "items": { + "type": "string" + } + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/DeleteSharepointSite": { + "post": { + "summary": "DeleteSharepointSite", + "tags": [ + "Teams-Sharepoint" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Sharepoint.Site.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "SiteId": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/DeleteTestReport": { + "post": { + "summary": "DeleteTestReport", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Dashboard.Read", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ReportId": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/DeployContactTemplates": { + "post": { + "summary": "DeployContactTemplates", + "tags": [ + "Email-Exchange > Administration > Contacts" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "This function deploys contact(s) from template(s) to selected tenants.\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", + "x-cipp-role": "Exchange.Contact.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "selectedTenants": { + "type": "string" + }, + "TemplateList": { + "type": "object", + "items": { + "type": "string" + }, + "properties": { + "Count": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "/api/EditAntiPhishingFilter": { + "post": { + "summary": "EditAntiPhishingFilter", + "tags": [ + "Email-Exchange > Spamfilter" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.SpamFilter.ReadWrite", + "parameters": [ + { + "name": "RuleName", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "State", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "RuleName": { + "type": "string" + }, + "State": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/EditAssignmentFilter": { + "post": { + "summary": "EditAssignmentFilter", + "tags": [ + "Endpoint > MEM" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.MEM.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "displayName": { + "type": "string" + }, + "description": { + "type": "string" + }, + "assignmentFilterManagementType": { + "type": "string", + "enum": [ + "devices", + "apps" + ] + }, + "platform": { + "type": "string" + }, + "rule": { + "type": "string" + }, + "filterId": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/EditCAPolicy": { + "post": { + "summary": "EditCAPolicy", + "tags": [ + "Tenant > Conditional" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.ConditionalAccess.ReadWrite", + "parameters": [ + { + "name": "GUID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "newDisplayName", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "State", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "GUID": { + "type": "string" + }, + "newDisplayName": { + "type": "string" + }, + "State": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/EditContact": { + "post": { + "summary": "EditContact", + "tags": [ + "Email-Exchange > Administration > Contacts" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Contact.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tenantID": { + "type": "string" + }, + "ContactID": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "hidefromGAL": { + "type": "boolean" + }, + "email": { + "type": "string" + }, + "firstName": { + "type": "string" + }, + "LastName": { + "type": "string" + }, + "Title": { + "type": "string" + }, + "StreetAddress": { + "type": "string" + }, + "PostalCode": { + "type": "string" + }, + "City": { + "type": "string" + }, + "State": { + "type": "string" + }, + "CountryOrRegion": { + "type": "string" + }, + "Company": { + "type": "string" + }, + "mobilePhone": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "website": { + "type": "string" + }, + "mailTip": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/EditContactTemplates": { + "post": { + "summary": "EditContactTemplates", + "tags": [ + "Email-Exchange > Administration > Contacts" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Contact.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ContactTemplateID": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "email": { + "type": "string" + }, + "hidefromGAL": { + "type": "boolean" + }, + "streetAddress": { + "type": "string" + }, + "postalCode": { + "type": "string" + }, + "city": { + "type": "string" + }, + "state": { + "type": "string" + }, + "country": { + "type": "string" + }, + "companyName": { + "type": "string" + }, + "mobilePhone": { + "type": "string" + }, + "businessPhone": { + "type": "string" + }, + "jobTitle": { + "type": "string" + }, + "website": { + "type": "string" + }, + "mailTip": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/EditEquipmentMailbox": { + "post": { + "summary": "EditEquipmentMailbox", + "tags": [ + "Email-Exchange > Resources" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Equipment.ReadWrite", + "x-cipp-warnings": [ + "customDataformatter is present on the CippFormPage for this endpoint. Form field names and/or types may be renamed or restructured before POST. Verify the transformer function in the frontend page file and correct param names/types accordingly." + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tenantID": { + "type": "string" + }, + "equipmentId": { + "type": "string" + }, + "hiddenFromAddressListsEnabled": { + "type": "boolean" + }, + "department": { + "type": "string" + }, + "company": { + "type": "string" + }, + "streetAddress": { + "type": "string" + }, + "city": { + "type": "string" + }, + "stateOrProvince": { + "type": "string" + }, + "postalCode": { + "type": "string" + }, + "countryOrRegion": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "allowConflicts": { + "type": "boolean" + }, + "allowRecurringMeetings": { + "type": "boolean" + }, + "bookingWindowInDays": { + "type": "integer" + }, + "maximumDurationInMinutes": { + "type": "integer" + }, + "processExternalMeetingMessages": { + "type": "boolean" + }, + "forwardRequestsToDelegates": { + "type": "boolean" + }, + "scheduleOnlyDuringWorkHours": { + "type": "boolean" + }, + "automateProcessing": { + "type": "string" + }, + "workDays": { + "type": "string" + }, + "workHoursStartTime": { + "type": "string" + }, + "workHoursEndTime": { + "type": "string" + }, + "workingHoursTimeZone": { + "type": "string" + }, + "userPrincipalName": { + "type": "string" + }, + "DisplayName": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/EditExConnector": { + "post": { + "summary": "EditExConnector", + "tags": [ + "Email-Exchange > Transport" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Connector.ReadWrite", + "parameters": [ + { + "name": "GUID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "State", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "Type", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "GUID": { + "type": "string" + }, + "State": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "Type": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/EditGroup": { + "patch": { + "summary": "EditGroup", + "tags": [ + "Identity > Administration > Groups" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Identity.Group.ReadWrite", + "x-cipp-warnings": [ + "customDataformatter is present on the CippFormPage for this endpoint. Form field names and/or types may be renamed or restructured before POST. Verify the transformer function in the frontend page file and correct param names/types accordingly." + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tenantFilter": { + "type": "string" + }, + "groupId": { + "type": "object", + "properties": { + "addedFields.groupName": { + "type": "string" + }, + "value": { + "type": "string" + }, + "addedFields.groupType": { + "type": "string" + } + } + }, + "groupType": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "description": { + "type": "string" + }, + "mailNickname": { + "type": "string" + }, + "membershipRules": { + "type": "string" + }, + "securityEnabled": { + "type": "boolean" + }, + "visibility": { + "type": "string" + }, + "allowExternal": { + "type": "boolean" + }, + "sendCopies": { + "type": "boolean" + }, + "hideFromOutlookClients": { + "type": "boolean" + }, + "mail": { + "type": "string" + }, + "AddMember": { + "type": "array", + "items": { + "type": "string" + } + }, + "RemoveMember": { + "type": "array", + "items": { + "type": "string" + } + }, + "AddOwner": { + "type": "array", + "items": { + "type": "string" + } + }, + "RemoveOwner": { + "type": "array", + "items": { + "type": "string" + } + }, + "AddContact": { + "type": "array", + "items": { + "type": "string" + } + }, + "RemoveContact": { + "type": "array", + "items": { + "type": "string" + } + }, + "groupName": { + "type": "string" + }, + "tenantId": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/EditIntunePolicy": { + "post": { + "summary": "EditIntunePolicy", + "tags": [ + "Endpoint > MEM" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.MEM.Read", + "parameters": [ + { + "name": "ID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "newDisplayName", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "policyType", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ID": { + "type": "string" + }, + "newDisplayName": { + "type": "string" + }, + "policyType": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/EditIntuneScript": { + "patch": { + "summary": "EditIntuneScript", + "tags": [ + "Endpoint > MEM" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.MEM.ReadWrite", + "parameters": [ + { + "name": "ScriptId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "IntuneScript": { + "type": "string" + }, + "ScriptId": { + "type": "string" + }, + "ScriptType": { + "type": "string" + }, + "TenantFilter": { + "type": "string" + } + }, + "required": [ + "TenantFilter" + ] + } + } + } + } + } + }, + "/api/EditJITAdminTemplate": { + "post": { + "summary": "EditJITAdminTemplate", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Identity.Role.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "defaultDomain": { + "$ref": "#/components/schemas/LabelValue" + }, + "defaultDuration": { + "$ref": "#/components/schemas/LabelValue" + }, + "defaultExistingUser": { + "$ref": "#/components/schemas/LabelValue" + }, + "defaultExpireAction": { + "$ref": "#/components/schemas/LabelValue" + }, + "defaultFirstName": { + "type": "string" + }, + "defaultForTenant": { + "type": "boolean" + }, + "defaultLastName": { + "type": "string" + }, + "defaultNotificationActions": { + "type": "array", + "items": { + "type": "string" + } + }, + "defaultRoles": { + "$ref": "#/components/schemas/LabelValue" + }, + "defaultUserAction": { + "type": "string" + }, + "defaultUserName": { + "type": "string" + }, + "generateTAPByDefault": { + "type": "boolean" + }, + "GUID": { + "type": "string" + }, + "reasonTemplate": { + "type": "string" + }, + "templateName": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/EditMalwareFilter": { + "post": { + "summary": "EditMalwareFilter", + "tags": [ + "Email-Exchange > Spamfilter" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.SpamFilter.ReadWrite", + "parameters": [ + { + "name": "RuleName", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "State", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "RuleName": { + "type": "string" + }, + "State": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/EditPolicy": { + "post": { + "summary": "EditPolicy", + "tags": [ + "Endpoint > MEM" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.MEM.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Assignto": { + "type": "string" + }, + "Description": { + "type": "string" + }, + "Displayname": { + "type": "string" + }, + "groupid": { + "type": "string" + }, + "tenantid": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/EditQuarantinePolicy": { + "post": { + "summary": "EditQuarantinePolicy", + "tags": [ + "Email-Exchange > Spamfilter" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Spamfilter.ReadWrite", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "Type", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Action": { + "type": "string" + }, + "AllowSender": { + "type": "string" + }, + "BlockSender": { + "type": "string" + }, + "Delete": { + "type": "string" + }, + "EndUserSpamNotificationCustomFromAddress": { + "type": "string" + }, + "EndUserSpamNotificationFrequency": { + "type": "string" + }, + "Identity": { + "type": "string" + }, + "IncludeMessagesFromBlockedSenderAddress": { + "type": "string" + }, + "Name": { + "type": "string" + }, + "OrganizationBrandingEnabled": { + "type": "string" + }, + "Preview": { + "type": "string" + }, + "QuarantineNotification": { + "type": "string" + }, + "ReleaseActionPreference": { + "type": "string" + }, + "TenantFilter": { + "type": "string" + } + }, + "required": [ + "TenantFilter" + ] + } + } + } + } + } + }, + "/api/EditRoomList": { + "post": { + "summary": "EditRoomList", + "tags": [ + "Email-Exchange > Resources" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Room.ReadWrite", + "x-cipp-warnings": [ + "customDataformatter is present on the CippFormPage for this endpoint. Form field names and/or types may be renamed or restructured before POST. Verify the transformer function in the frontend page file and correct param names/types accordingly." + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tenantFilter": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "description": { + "type": "string" + }, + "mailNickname": { + "type": "string" + }, + "AddMember": { + "type": "array", + "items": { + "type": "string" + } + }, + "RemoveMember": { + "type": "array", + "items": { + "type": "string" + } + }, + "AddOwner": { + "type": "array", + "items": { + "type": "string" + } + }, + "RemoveOwner": { + "type": "array", + "items": { + "type": "string" + } + }, + "allowExternal": { + "type": "boolean" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/EditRoomMailbox": { + "post": { + "summary": "EditRoomMailbox", + "tags": [ + "Email-Exchange > Resources" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Room.ReadWrite", + "x-cipp-warnings": [ + "customDataformatter is present on the CippFormPage for this endpoint. Form field names and/or types may be renamed or restructured before POST. Verify the transformer function in the frontend page file and correct param names/types accordingly." + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tenantID": { + "type": "string" + }, + "roomId": { + "type": "string" + }, + "hiddenFromAddressListsEnabled": { + "type": "boolean" + }, + "capacity": { + "type": "integer" + }, + "building": { + "type": "string" + }, + "floor": { + "type": "integer" + }, + "floorLabel": { + "type": "string" + }, + "street": { + "type": "string" + }, + "city": { + "type": "string" + }, + "state": { + "type": "string" + }, + "postalCode": { + "type": "string" + }, + "countryOrRegion": { + "type": "string" + }, + "audioDeviceName": { + "type": "string" + }, + "videoDeviceName": { + "type": "string" + }, + "displayDeviceName": { + "type": "string" + }, + "isWheelChairAccessible": { + "type": "boolean" + }, + "phone": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "AllowConflicts": { + "type": "boolean" + }, + "AllowRecurringMeetings": { + "type": "boolean" + }, + "BookingWindowInDays": { + "type": "integer" + }, + "MaximumDurationInMinutes": { + "type": "integer" + }, + "ProcessExternalMeetingMessages": { + "type": "boolean" + }, + "EnforceCapacity": { + "type": "boolean" + }, + "ForwardRequestsToDelegates": { + "type": "boolean" + }, + "ScheduleOnlyDuringWorkHours": { + "type": "boolean" + }, + "AutomateProcessing": { + "type": "string" + }, + "AddOrganizerToSubject": { + "type": "boolean" + }, + "DeleteSubject": { + "type": "boolean" + }, + "RemoveCanceledMeetings": { + "type": "boolean" + }, + "WorkDays": { + "type": "string" + }, + "WorkHoursStartTime": { + "type": "string" + }, + "WorkHoursEndTime": { + "type": "string" + }, + "WorkingHoursTimeZone": { + "type": "string" + }, + "userPrincipalName": { + "type": "string" + }, + "DisplayName": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/EditSafeAttachmentsFilter": { + "post": { + "summary": "EditSafeAttachmentsFilter", + "tags": [ + "Email-Exchange > Spamfilter" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.SpamFilter.ReadWrite", + "parameters": [ + { + "name": "RuleName", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "State", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "RuleName": { + "type": "string" + }, + "State": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/EditSafeLinksPolicy": { + "post": { + "summary": "EditSafeLinksPolicy", + "tags": [ + "Security > Safe-Links-Policy" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "This function modifies an existing Safe Links policy and its associated rule.\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", + "x-cipp-role": "Exchange.SpamFilter.ReadWrite", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "PolicyName", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "RuleName", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tenantFilter": { + "type": "string" + }, + "PolicyName": { + "type": "string" + }, + "RuleName": { + "type": "string" + }, + "EnableSafeLinksForEmail": { + "type": "boolean" + }, + "EnableSafeLinksForTeams": { + "type": "boolean" + }, + "EnableSafeLinksForOffice": { + "type": "boolean" + }, + "TrackClicks": { + "type": "boolean" + }, + "AllowClickThrough": { + "type": "boolean" + }, + "ScanUrls": { + "type": "boolean" + }, + "EnableForInternalSenders": { + "type": "boolean" + }, + "DeliverMessageAfterScan": { + "type": "boolean" + }, + "DisableUrlRewrite": { + "type": "boolean" + }, + "EnableOrganizationBranding": { + "type": "boolean" + }, + "AdminDisplayName": { + "type": "string" + }, + "CustomNotificationText": { + "type": "string" + }, + "DoNotRewriteUrls": { + "type": "array", + "items": { + "type": "string" + } + }, + "Priority": { + "type": "integer" + }, + "Comments": { + "type": "string" + }, + "State": { + "type": "boolean" + }, + "SentTo": { + "type": "array", + "items": { + "type": "string" + } + }, + "SentToMemberOf": { + "type": "array", + "items": { + "type": "string" + } + }, + "RecipientDomainIs": { + "type": "array", + "items": { + "type": "string" + } + }, + "ExceptIfSentTo": { + "type": "array", + "items": { + "type": "string" + } + }, + "ExceptIfSentToMemberOf": { + "type": "array", + "items": { + "type": "string" + } + }, + "ExceptIfRecipientDomainIs": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/EditSafeLinksPolicyTemplate": { + "post": { + "summary": "EditSafeLinksPolicyTemplate", + "tags": [ + "Security > Safe-Links-Policy" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "This function updates an existing Safe Links policy template.\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", + "x-cipp-role": "Exchange.SafeLinks.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ID": { + "type": "string" + }, + "TemplateName": { + "type": "string" + }, + "TemplateDescription": { + "type": "string" + }, + "PolicyName": { + "type": "string" + }, + "AdminDisplayName": { + "type": "string" + }, + "EnableSafeLinksForEmail": { + "type": "boolean" + }, + "EnableSafeLinksForTeams": { + "type": "boolean" + }, + "EnableSafeLinksForOffice": { + "type": "boolean" + }, + "TrackClicks": { + "type": "boolean" + }, + "AllowClickThrough": { + "type": "boolean" + }, + "ScanUrls": { + "type": "boolean" + }, + "EnableForInternalSenders": { + "type": "boolean" + }, + "DeliverMessageAfterScan": { + "type": "boolean" + }, + "DisableUrlRewrite": { + "type": "boolean" + }, + "EnableOrganizationBranding": { + "type": "boolean" + }, + "DoNotRewriteUrls": { + "type": "array", + "items": { + "type": "string" + } + }, + "CustomNotificationText": { + "type": "string" + }, + "RuleName": { + "type": "string" + }, + "Priority": { + "type": "integer" + }, + "State": { + "type": "string" + }, + "Comments": { + "type": "string" + }, + "SentTo": { + "type": "array", + "items": { + "type": "string" + } + }, + "SentToMemberOf": { + "type": "array", + "items": { + "type": "string" + } + }, + "RecipientDomainIs": { + "type": "array", + "items": { + "type": "string" + } + }, + "ExceptIfSentTo": { + "type": "array", + "items": { + "type": "string" + } + }, + "ExceptIfSentToMemberOf": { + "type": "array", + "items": { + "type": "string" + } + }, + "ExceptIfRecipientDomainIs": { + "type": "array", + "items": { + "type": "string" + } + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/EditSpamFilter": { + "post": { + "summary": "EditSpamFilter", + "tags": [ + "Email-Exchange > Spamfilter" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.SpamFilter.ReadWrite", + "parameters": [ + { + "name": "name", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "state": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/EditTenant": { + "post": { + "summary": "EditTenant", + "tags": [ + "Tenant > Administration > Tenant" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Config.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tenantGroups": { + "type": "string" + }, + "tenantAlias": { + "type": "string" + }, + "customerId": { + "type": "string" + }, + "GroupId": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/EditTenantOffboardingDefaults": { + "post": { + "summary": "EditTenantOffboardingDefaults", + "tags": [ + "Tenant > Administration > Tenant" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Config.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Alias": { + "type": "string" + }, + "customerId": { + "type": "string" + }, + "defaultDomainName": { + "type": "string" + }, + "Groups": { + "$ref": "#/components/schemas/LabelValue" + }, + "offboardingDefaults": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/EditTransportRule": { + "post": { + "summary": "EditTransportRule", + "tags": [ + "Email-Exchange > Transport" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.TransportRule.ReadWrite", + "parameters": [ + { + "name": "guid", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "state", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "guid": { + "type": "string" + }, + "state": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/EditUser": { + "patch": { + "summary": "EditUser", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Identity.User.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "userTemplate": { + "$ref": "#/components/schemas/LabelValue" + }, + "givenName": { + "type": "string" + }, + "surname": { + "type": "string" + }, + "Autopassword": { + "type": "boolean" + }, + "password": { + "type": "string", + "format": "password" + }, + "MustChangePass": { + "type": "boolean" + }, + "usageLocation": { + "$ref": "#/components/schemas/LabelValue", + "type": "object", + "properties": { + "value": { + "type": "string" + } + } + }, + "sherweb": { + "type": "boolean" + }, + "removeLicenses": { + "type": "boolean" + }, + "jobTitle": { + "type": "string" + }, + "streetAddress": { + "type": "string" + }, + "city": { + "type": "string" + }, + "state": { + "type": "string" + }, + "postalCode": { + "type": "string" + }, + "country": { + "type": "string" + }, + "companyName": { + "type": "string" + }, + "department": { + "type": "string" + }, + "mobilePhone": { + "type": "string" + }, + "businessPhones": { + "type": "string" + }, + "otherMails": { + "type": "string" + }, + "AddToGroups": { + "type": "array", + "items": { + "type": "string" + } + }, + "RemoveFromGroups": { + "type": "array", + "items": { + "type": "string" + } + }, + "Scheduled": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "date": { + "type": "string", + "format": "date-time" + } + } + }, + "postExecution": { + "type": "object", + "properties": { + "webhook": { + "type": "boolean" + }, + "email": { + "type": "boolean" + }, + "psa": { + "type": "boolean" + } + } + }, + "reference": { + "type": "string" + }, + "primDomain": { + "$ref": "#/components/schemas/LabelValue", + "type": "object", + "properties": { + "value": { + "type": "string" + } + } + }, + "licenses": { + "type": "array", + "items": { + "type": "string" + } + }, + "setManager": { + "$ref": "#/components/schemas/LabelValue" + }, + "setSponsor": { + "$ref": "#/components/schemas/LabelValue" + }, + "PostExecution": { + "type": "string" + }, + "id": { + "type": "string" + }, + "username": { + "type": "string" + }, + "Domain": { + "type": "string" + }, + "mailNickname": { + "type": "string" + }, + "defaultAttributes": { + "type": "string" + }, + "customData": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "sherwebLicense": { + "type": "object", + "properties": { + "value": { + "type": "string" + } + } + }, + "userPrincipalName": { + "type": "string" + }, + "CopyFrom": { + "type": "string" + }, + "DisplayName": { + "type": "string" + }, + "AddedAliases": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/EditUserAliases": { + "post": { + "summary": "EditUserAliases", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Identity.User.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "AddedAliases": { + "type": "string" + }, + "id": { + "type": "string" + }, + "MakePrimary": { + "type": "string" + }, + "RemovedAliases": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecAPIPermissionList": { + "get": { + "summary": "ExecAPIPermissionList", + "tags": [ + "CIPP > Settings" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.SuperAdmin.Read" + } + }, + "/api/ExecAccessChecks": { + "post": { + "summary": "ExecAccessChecks", + "tags": [ + "CIPP > Settings" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.AppSettings.Read", + "parameters": [ + { + "name": "SkipCache", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Type", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "TenantId": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecAddAlert": { + "post": { + "summary": "ExecAddAlert", + "tags": [ + "CIPP > Core" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Alert.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "logsToInclude": { + "type": "string" + }, + "onePerTenant": { + "type": "string" + }, + "sendEmailNow": { + "type": "string" + }, + "sendPsaNow": { + "type": "string" + }, + "sendWebhookNow": { + "type": "string" + }, + "Severity": { + "type": "string" + }, + "text": { + "type": "string" + }, + "webhook": { + "type": "string" + }, + "writeLog": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecAddGDAPRole": { + "post": { + "summary": "ExecAddGDAPRole", + "tags": [ + "Tenant > GDAP" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Relationship.ReadWrite", + "x-cipp-warnings": [ + "advancedMappings state is held in the component, not in RHF form values. In AddRoleAdvanced mode, only Action and Mappings are sent — all form field values are discarded." + ], + "parameters": [ + { + "name": "Action", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "gdapTemplate": { + "$ref": "#/components/schemas/LabelValue" + }, + "inviteCount": { + "type": "number" + }, + "Reference": { + "type": "string" + }, + "Action": { + "type": "string" + }, + "gdapRoles": { + "type": "array", + "items": { + "type": "string" + } + }, + "customSuffix": { + "type": "string" + }, + "templateId": { + "type": "string" + }, + "mappings": { + "type": "string" + }, + "replace": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecAddMultiTenantApp": { + "post": { + "summary": "ExecAddMultiTenantApp", + "tags": [ + "Tenant > Administration > Application Approval" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Application.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "AppId": { + "type": "string" + }, + "configMode": { + "type": "string" + }, + "CopyPermissions": { + "type": "string" + }, + "permissions": { + "type": "string" + }, + "selectedTemplate": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecAddSPN": { + "get": { + "summary": "ExecAddSPN", + "tags": [ + "Tenant > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Administration.ReadWrite" + } + }, + "/api/ExecAddTenant": { + "post": { + "summary": "ExecAddTenant", + "tags": [ + "CIPP > Setup" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.AppSettings.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "accessToken": { + "type": "string" + }, + "defaultDomainName": { + "type": "string" + }, + "tenantId": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecAddTrustedIP": { + "post": { + "summary": "ExecAddTrustedIP", + "tags": [ + "CIPP > Settings" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.AppSettings.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "IP": { + "type": "string" + }, + "ipAddress": { + "type": "string" + }, + "State": { + "type": "string" + }, + "tenantfilter": { + "type": "string" + } + }, + "required": [ + "tenantfilter" + ] + } + } + } + } + } + }, + "/api/ExecAlertsList": { + "get": { + "summary": "ExecAlertsList", + "tags": [ + "Security" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Security.Alert.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ExecApiClient": { + "delete": { + "summary": "ExecApiClient", + "tags": [ + "CIPP > Settings" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Extension.ReadWrite", + "parameters": [ + { + "name": "Action", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Action": { + "type": "string" + }, + "AppName": { + "type": "string" + }, + "CIPPAPI": { + "type": "string" + }, + "ClientId": { + "type": "string" + }, + "Enabled": { + "type": "boolean" + }, + "IpRange": { + "type": "string" + }, + "RemoveAppReg": { + "type": "string" + }, + "Role": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecAppApproval": { + "get": { + "summary": "ExecAppApproval", + "tags": [ + "Tenant > Administration > Application Approval" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Application.Read", + "parameters": [ + { + "name": "ApplicationId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/ExecAppApprovalTemplate": { + "delete": { + "summary": "ExecAppApprovalTemplate", + "tags": [ + "Tenant > Administration > Application Approval" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Application.ReadWrite", + "parameters": [ + { + "name": "Action", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "TemplateId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Action": { + "type": "string" + }, + "TemplateId": { + "type": "string" + }, + "TemplateName": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecAppInsightsQuery": { + "get": { + "summary": "ExecAppInsightsQuery", + "tags": [ + "CIPP > Core" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.SuperAdmin.Read" + } + }, + "/api/ExecAppPermissionTemplate": { + "delete": { + "summary": "ExecAppPermissionTemplate", + "tags": [ + "Tenant > Administration > Application Approval" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.ApplicationTemplates.ReadWrite", + "parameters": [ + { + "name": "Action", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "TemplateId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Action": { + "type": "string" + }, + "Permissions": { + "type": "string" + }, + "TemplateId": { + "type": "string" + }, + "TemplateName": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecAppUpload": { + "get": { + "summary": "ExecAppUpload", + "tags": [ + "Endpoint > Applications" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.Application.ReadWrite" + } + }, + "/api/ExecApplication": { + "patch": { + "summary": "ExecApplication", + "tags": [ + "Tenant > Administration > Application Approval" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Application.ReadWrite", + "parameters": [ + { + "name": "Action", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "AppId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Id", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "Type", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Action": { + "type": "string" + }, + "AppId": { + "type": "string" + }, + "Id": { + "type": "string" + }, + "KeyIds": { + "type": "string" + }, + "Payload": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "Type": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecAssignAPDevice": { + "post": { + "summary": "ExecAssignAPDevice", + "tags": [ + "Endpoint > Autopilot" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.Autopilot.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "device": { + "type": "string" + }, + "serialNumber": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "user": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecAssignApp": { + "post": { + "summary": "ExecAssignApp", + "tags": [ + "Endpoint > Applications" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.Application.ReadWrite", + "parameters": [ + { + "name": "AppType", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "AssignTo", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "GroupIds", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "GroupNames", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "ID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Intent", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "AppType": { + "type": "string" + }, + "AssignmentFilterName": { + "type": "string" + }, + "AssignmentFilterType": { + "type": "string" + }, + "assignmentMode": { + "type": "string" + }, + "AssignTo": { + "type": "string" + }, + "GroupIds": { + "type": "string" + }, + "GroupNames": { + "type": "string" + }, + "ID": { + "type": "string" + }, + "Intent": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecAssignPolicy": { + "post": { + "summary": "ExecAssignPolicy", + "tags": [ + "Endpoint > MEM" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.MEM.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "AssignmentFilterName": { + "type": "string" + }, + "AssignmentFilterType": { + "type": "string" + }, + "assignmentMode": { + "type": "string" + }, + "AssignTo": { + "type": "string" + }, + "excludeGroup": { + "type": "string" + }, + "GroupIds": { + "type": "string" + }, + "GroupNames": { + "type": "string" + }, + "ID": { + "type": "string" + }, + "platformType": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "Type": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecAssignmentFilter": { + "delete": { + "summary": "ExecAssignmentFilter", + "tags": [ + "Endpoint > MEM" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.MEM.ReadWrite", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Action": { + "type": "string" + }, + "ID": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecAuditLogSearch": { + "post": { + "summary": "ExecAuditLogSearch", + "tags": [ + "Tenant > Administration > Alerts" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Alert.ReadWrite", + "parameters": [ + { + "name": "Action", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "SearchId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Action": { + "type": "string" + }, + "EndTime": { + "type": "string" + }, + "SearchId": { + "type": "string" + }, + "StartTime": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "PSObject": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecAutoExtendGDAP": { + "post": { + "summary": "ExecAutoExtendGDAP", + "tags": [ + "Tenant > GDAP" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Relationship.ReadWrite", + "parameters": [ + { + "name": "ID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ID": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecAzBobbyTables": { + "post": { + "summary": "Execute a AzBobbyTables function", + "tags": [ + "CIPP > Core" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "This function is used to interact with Azure Tables. This is advanced functionality used for external integrations or SuperAdmin functionality.\n .FUNCTIONALITY\n Entrypoint\n .ROLE\n CIPP.SuperAdmin.ReadWrite\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)\n $AllowList = @(\n 'Add-AzDataTableEntity'\n 'Add-CIPPAzDataTableEntity'\n 'Update-AzDataTableEntity'\n 'Get-AzDataTableEntity'\n 'Get-CIPPAzDataTableEntity'\n 'Get-AzDataTable'\n 'New-AzDataTable'\n 'Remove-AzDataTableEntity'\n 'Remove-AzDataTable'\n )", + "x-cipp-role": "CIPP.SuperAdmin.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "FunctionName": { + "type": "string" + }, + "OffloadFunctions": { + "type": "boolean" + }, + "Parameters": { + "type": "string" + }, + "TableName": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecBECCheck": { + "get": { + "summary": "ExecBECCheck", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Identity.User.Read", + "parameters": [ + { + "name": "GUID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "overwrite", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "userid", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "userName", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/ExecBECRemediate": { + "post": { + "summary": "ExecBECRemediate", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Identity.User.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tenantFilter": { + "type": "string" + }, + "userid": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecBPA": { + "post": { + "summary": "ExecBPA", + "tags": [ + "Tenant > Standards" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.BestPracticeAnalyser.ReadWrite", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tenantfilter": { + "type": "string" + } + }, + "required": [ + "tenantfilter" + ] + } + } + } + } + } + }, + "/api/ExecBackendURLs": { + "get": { + "summary": "ExecBackendURLs", + "tags": [ + "CIPP > Settings" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.AppSettings.Read" + } + }, + "/api/ExecBackupRetentionConfig": { + "post": { + "summary": "ExecBackupRetentionConfig", + "tags": [ + "CIPP > Settings" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.AppSettings.ReadWrite", + "parameters": [ + { + "name": "List", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "RetentionDays": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecBitlockerSearch": { + "post": { + "summary": "ExecBitlockerSearch", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.Device.Read", + "parameters": [ + { + "name": "deviceId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "keyId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "deviceId": { + "type": "string" + }, + "keyId": { + "type": "string" + }, + "limit": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecBrandingSettings": { + "post": { + "summary": "ExecBrandingSettings", + "tags": [ + "CIPP > Settings" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.AppSettings.ReadWrite", + "parameters": [ + { + "name": "Action", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Action": { + "type": "string" + }, + "colour": { + "type": "string" + }, + "logo": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecBreachSearch": { + "post": { + "summary": "ExecBreachSearch", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Core.Read", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecBulkLicense": { + "get": { + "summary": "ExecBulkLicense", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Identity.User.ReadWrite" + } + }, + "/api/ExecCACheck": { + "post": { + "summary": "ExecCACheck", + "tags": [ + "Tenant > Conditional" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.ConditionalAccess.Read", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "authenticationFlow": { + "$ref": "#/components/schemas/LabelValue" + }, + "ClientAppType": { + "$ref": "#/components/schemas/LabelValue" + }, + "Country": { + "$ref": "#/components/schemas/LabelValue" + }, + "DevicePlatform": { + "$ref": "#/components/schemas/LabelValue" + }, + "IncludeApplications": { + "$ref": "#/components/schemas/LabelValue" + }, + "IpAddress": { + "type": "string" + }, + "SignInRiskLevel": { + "$ref": "#/components/schemas/LabelValue" + }, + "tenantFilter": { + "type": "string" + }, + "userID": { + "type": "string" + }, + "UserRiskLevel": { + "$ref": "#/components/schemas/LabelValue" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecCAExclusion": { + "post": { + "summary": "ExecCAExclusion", + "tags": [ + "Tenant > Conditional" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.ConditionalAccess.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tenantFilter": { + "type": "string" + }, + "Users": { + "type": "array", + "items": { + "type": "string" + } + }, + "PolicyId": { + "type": "string" + }, + "StartDate": { + "type": "integer" + }, + "EndDate": { + "type": "integer" + }, + "vacation": { + "type": "boolean" + }, + "postExecution": { + "type": "array", + "items": { + "type": "string" + } + }, + "reference": { + "type": "string" + }, + "UserID": { + "type": "string" + }, + "Username": { + "type": "string" + }, + "ExclusionType": { + "type": "string" + }, + "excludeLocationAuditAlerts": { + "type": "string" + }, + "value": { + "type": "string" + }, + "addedFields": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecCAServiceExclusion": { + "post": { + "summary": "ExecCAServiceExclusion", + "tags": [ + "Tenant > Conditional" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.ConditionalAccess.ReadWrite", + "parameters": [ + { + "name": "GUID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "GUID": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecCIPPDBCache": { + "get": { + "summary": "ExecCIPPDBCache", + "tags": [ + "CIPP > Core" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Core.ReadWrite", + "parameters": [ + { + "name": "Name", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "Types", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/ExecCPVPermissions": { + "post": { + "summary": "ExecCPVPermissions", + "tags": [ + "CIPP > Settings" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.AppSettings.ReadWrite", + "parameters": [ + { + "name": "ResetSP", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecCPVRefresh": { + "get": { + "summary": "This endpoint is used to trigger a refresh of CPV for all tenants", + "tags": [ + "CIPP > Core" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Core.ReadWrite" + } + }, + "/api/ExecCSPLicense": { + "get": { + "summary": "ExecCSPLicense", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Directory.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Action": { + "type": "string" + }, + "Add": { + "type": "string" + }, + "iagree": { + "type": "boolean" + }, + "Quantity": { + "type": "number" + }, + "Remove": { + "type": "string" + }, + "SKU": { + "$ref": "#/components/schemas/LabelValue" + }, + "SubscriptionIds": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + }, + "post": { + "summary": "ExecCSPLicense", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Directory.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Action": { + "type": "string" + }, + "Add": { + "type": "string" + }, + "iagree": { + "type": "boolean" + }, + "Quantity": { + "type": "number" + }, + "Remove": { + "type": "string" + }, + "SKU": { + "$ref": "#/components/schemas/LabelValue" + }, + "SubscriptionIds": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecCippFunction": { + "post": { + "summary": "Execute a CIPPCore function", + "tags": [ + "CIPP > Core" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "This function is used to execute a CIPPCore function from an HTTP request. This is advanced functionality used for external integrations or SuperAdmin functionality.\n .FUNCTIONALITY\n Entrypoint\n .ROLE\n CIPP.SuperAdmin.ReadWrite\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)\n $BlockList = @(\n 'Get-GraphToken'\n 'Get-GraphTokenFromCert'\n 'Get-ClassicAPIToken'\n )", + "x-cipp-role": "CIPP.SuperAdmin.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "FunctionName": { + "type": "string" + }, + "Parameters": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecCippLogsSas": { + "post": { + "summary": "Generate a read-only SAS token for the CippLogs table", + "tags": [ + "CIPP > Core" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Creates a long-lived, read-only SAS URL for the CippLogs Azure Storage table.\n .FUNCTIONALITY\n Entrypoint\n .ROLE\n CIPP.AppSettings.ReadWrite\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", + "x-cipp-role": "CIPP.AppSettings.ReadWrite", + "parameters": [ + { + "name": "Days", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Days": { + "$ref": "#/components/schemas/LabelValue" + } + } + } + } + } + } + } + }, + "/api/ExecCippReplacemap": { + "delete": { + "summary": "ExecCippReplacemap", + "tags": [ + "CIPP > Settings" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Config.ReadWrite", + "parameters": [ + { + "name": "Action", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "tenantId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Action": { + "type": "string" + }, + "Description": { + "type": "string" + }, + "RowKey": { + "type": "string" + }, + "tenantId": { + "type": "string" + }, + "Value": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecCloneTemplate": { + "post": { + "summary": "ExecCloneTemplate", + "tags": [ + "CIPP > Core" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Core.ReadWrite", + "parameters": [ + { + "name": "GUID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Type", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "GUID": { + "type": "string" + }, + "Type": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecClrImmId": { + "post": { + "summary": "ExecClrImmId", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Identity.User.ReadWrite", + "parameters": [ + { + "name": "ID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ID": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecCombinedSetup": { + "post": { + "summary": "ExecCombinedSetup", + "tags": [ + "CIPP > Setup" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.AppSettings.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "applicationId": { + "type": "string" + }, + "applicationSecret": { + "type": "string" + }, + "baselineOption": { + "type": "string" + }, + "email": { + "type": "string" + }, + "RefreshToken": { + "type": "string" + }, + "selectedBaselines": { + "type": "string" + }, + "selectedOption": { + "type": "string" + }, + "tenantid": { + "type": "string" + }, + "webhook": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecCommunityRepo": { + "delete": { + "summary": "Make changes to a community repository", + "tags": [ + "Tools > GitHub" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "This function makes changes to a community repository in table storage\n .FUNCTIONALITY\n Entrypoint,AnyTenant\n .ROLE\n CIPP.Core.ReadWrite\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", + "x-cipp-role": "CIPP.Core.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Action": { + "type": "string" + }, + "Branch": { + "type": "string" + }, + "Description": { + "type": "string" + }, + "FullName": { + "type": "string" + }, + "GUID": { + "type": "string" + }, + "Id": { + "type": "string" + }, + "includeforks": { + "type": "boolean" + }, + "Message": { + "type": "string" + }, + "orgName": { + "$ref": "#/components/schemas/LabelValue" + }, + "Path": { + "type": "string" + }, + "policySource": { + "$ref": "#/components/schemas/LabelValue" + }, + "Private": { + "type": "boolean" + }, + "repoName": { + "type": "string" + }, + "searchTerm": { + "$ref": "#/components/schemas/LabelValue" + } + } + } + } + } + } + } + }, + "/api/ExecConvertMailbox": { + "post": { + "summary": "ExecConvertMailbox", + "tags": [ + "Email-Exchange > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ID": { + "type": "string" + }, + "MailboxType": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecCopyForSent": { + "post": { + "summary": "ExecCopyForSent", + "tags": [ + "Email-Exchange > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "parameters": [ + { + "name": "ID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "messageCopyState", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ID": { + "type": "string" + }, + "messageCopyState": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecCreateAppTemplate": { + "post": { + "summary": "ExecCreateAppTemplate", + "tags": [ + "Tenant > Administration > Application Approval" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Application.ReadWrite", + "x-cipp-dynamic-options": true, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "AppId": { + "type": "string" + }, + "DisplayName": { + "type": "string" + }, + "Overwrite": { + "type": "string" + }, + "TenantFilter": { + "type": "string" + }, + "Type": { + "type": "string" + } + }, + "required": [ + "TenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecCreateDefaultGroups": { + "get": { + "summary": "Create default tenant groups", + "tags": [ + "CIPP > Settings" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "This function creates a set of default tenant groups that are commonly used\n .FUNCTIONALITY\n Entrypoint,AnyTenant\n .ROLE\n Tenant.Groups.ReadWrite\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", + "x-cipp-role": "Tenant.Groups.ReadWrite" + } + }, + "/api/ExecCreateSAMApp": { + "post": { + "summary": "ExecCreateSAMApp", + "tags": [ + "CIPP > Setup" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.AppSettings.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecCreateTAP": { + "post": { + "summary": "ExecCreateTAP", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Identity.User.ReadWrite", + "parameters": [ + { + "name": "ID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "isUsableOnce", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "lifetimeInMinutes", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "startDateTime", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ID": { + "type": "string" + }, + "isUsableOnce": { + "type": "string" + }, + "lifetimeInMinutes": { + "type": "string" + }, + "startDateTime": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecCustomData": { + "post": { + "summary": "ExecCustomData", + "tags": [ + "CIPP > Settings" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.AppSettings.ReadWrite", + "parameters": [ + { + "name": "Action", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "targetObject", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Action": { + "type": "string" + }, + "dataType": { + "type": "string" + }, + "id": { + "type": "string" + }, + "isMultiValued": { + "type": "string" + }, + "Mapping": { + "type": "string" + }, + "name": { + "type": "string" + }, + "schemaExtension": { + "type": "string" + }, + "status": { + "type": "string" + }, + "targetObjects": { + "type": "string" + }, + "type": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecCustomRole": { + "delete": { + "summary": "ExecCustomRole", + "tags": [ + "CIPP > Settings" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.SuperAdmin.ReadWrite", + "parameters": [ + { + "name": "Action", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Action": { + "type": "string" + }, + "AllowedTenants": { + "type": "string" + }, + "BlockedEndpoints": { + "type": "string" + }, + "BlockedTenants": { + "type": "string" + }, + "EntraGroup": { + "type": "string" + }, + "IpRange": { + "type": "string" + }, + "NewRoleName": { + "type": "string" + }, + "Permissions": { + "type": "string" + }, + "RoleName": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecDeleteGDAPRelationship": { + "post": { + "summary": "ExecDeleteGDAPRelationship", + "tags": [ + "Tenant > GDAP" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Relationship.ReadWrite", + "parameters": [ + { + "name": "GDAPId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "GDAPId": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecDeleteGDAPRoleMapping": { + "post": { + "summary": "ExecDeleteGDAPRoleMapping", + "tags": [ + "Tenant > GDAP" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Relationship.ReadWrite", + "parameters": [ + { + "name": "GroupId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "GroupId": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecDeleteSafeLinksPolicy": { + "post": { + "summary": "ExecDeleteSafeLinksPolicy", + "tags": [ + "Security > Safe-Links-Policy" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "This function deletes a Safe Links rule and its associated policy.\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)\n $APIName = $Request.Params.CIPPEndpoint\n $Headers = $Request.Headers", + "x-cipp-role": "Exchange.SpamFilter.ReadWrite", + "parameters": [ + { + "name": "PolicyName", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "RuleName", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "PolicyName": { + "type": "string" + }, + "RuleName": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecDeviceAction": { + "post": { + "summary": "ExecDeviceAction", + "tags": [ + "Endpoint > MEM" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.MEM.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Action": { + "type": "string" + }, + "GUID": { + "type": "string" + }, + "input": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "user": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecDeviceCodeLogon": { + "get": { + "summary": "ExecDeviceCodeLogon", + "tags": [ + "CIPP > Setup" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.AppSettings.ReadWrite", + "parameters": [ + { + "name": "clientId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "deviceCode", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "operation", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "scope", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "tenantId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/ExecDeviceDelete": { + "post": { + "summary": "ExecDeviceDelete", + "tags": [ + "Identity > Administration > Devices" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Identity.Device.ReadWrite", + "parameters": [ + { + "name": "action", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "ID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "action": { + "type": "string" + }, + "ID": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecDevicePasscodeAction": { + "post": { + "summary": "ExecDevicePasscodeAction", + "tags": [ + "Endpoint > MEM" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.MEM.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Action": { + "type": "string" + }, + "GUID": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecDiagnosticsPresets": { + "delete": { + "summary": "ExecDiagnosticsPresets", + "tags": [ + "CIPP > Core" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.SuperAdmin.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "queryPreset": { + "$ref": "#/components/schemas/LabelValue" + }, + "name": { + "type": "string" + }, + "action": { + "type": "string" + }, + "GUID": { + "type": "string" + }, + "query": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecDisableUser": { + "post": { + "summary": "ExecDisableUser", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Identity.User.ReadWrite", + "parameters": [ + { + "name": "Enable", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "ID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Enable": { + "type": "string" + }, + "ID": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecDismissRiskyUser": { + "post": { + "summary": "ExecDismissRiskyUser", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Identity.User.ReadWrite", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "userDisplayName", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "userId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tenantFilter": { + "type": "string" + }, + "userDisplayName": { + "type": "string" + }, + "userId": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecDnsConfig": { + "post": { + "summary": "ExecDnsConfig", + "tags": [ + "CIPP > Settings" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Domains.ReadWrite", + "parameters": [ + { + "name": "Action", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Domain", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Resolver", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Selector", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Action": { + "type": "string" + }, + "Domain": { + "type": "string" + }, + "Resolver": { + "type": "string" + }, + "Selector": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecDomainAction": { + "delete": { + "summary": "ExecDomainAction", + "tags": [ + "Tenant > Administration > Domains" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Administration.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Action": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecDomainAnalyser": { + "post": { + "summary": "ExecDomainAnalyser", + "tags": [ + "Tenant > Standards" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.DomainAnalyser.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecDriftClone": { + "post": { + "summary": "ExecDriftClone", + "tags": [ + "Tenant > Standards" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Standards.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecDurableFunctions": { + "get": { + "summary": "ExecDurableFunctions", + "tags": [ + "CIPP > Core" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.AppSettings.ReadWrite", + "x-cipp-dynamic-options": true, + "parameters": [ + { + "name": "Action", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "PartitionKey", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/ExecEditCalendarPermissions": { + "post": { + "summary": "ExecEditCalendarPermissions", + "tags": [ + "Email-Exchange > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "parameters": [ + { + "name": "CanViewPrivateItems", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "FolderName", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Permissions", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "RemoveAccess", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "userid", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "UserToGetPermissions", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "CanViewPrivateItems": { + "type": "string" + }, + "FolderName": { + "type": "string" + }, + "Permissions": { + "type": "string" + }, + "RemoveAccess": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "userid": { + "type": "string" + }, + "UserToGetPermissions": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecEditMailboxPermissions": { + "post": { + "summary": "ExecEditMailboxPermissions", + "tags": [ + "Email-Exchange > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "AddFullAccess": { + "type": "string" + }, + "AddFullAccessNoAutoMap": { + "type": "string" + }, + "AddSendAs": { + "type": "string" + }, + "AddSendOnBehalf": { + "type": "string" + }, + "RemoveFullAccess": { + "type": "string" + }, + "RemoveSendAs": { + "type": "string" + }, + "RemoveSendOnBehalf": { + "type": "string" + }, + "tenantfilter": { + "type": "string" + }, + "userID": { + "type": "string" + } + }, + "required": [ + "tenantfilter" + ] + } + } + } + } + } + }, + "/api/ExecEditTemplate": { + "post": { + "summary": "ExecEditTemplate", + "tags": [ + "CIPP > Core" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Core.ReadWrite", + "x-cipp-dynamic-options": true, + "parameters": [ + { + "name": "Type", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "GUID": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "parsedRAWJson": { + "type": "string" + }, + "Type": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecEmailForward": { + "post": { + "summary": "ExecEmailForward", + "tags": [ + "Email-Exchange > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ForwardExternal": { + "type": "string" + }, + "ForwardInternal": { + "type": "string" + }, + "forwardOption": { + "type": "string" + }, + "KeepCopy": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "userID": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecEnableArchive": { + "post": { + "summary": "ExecEnableArchive", + "tags": [ + "Email-Exchange > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "parameters": [ + { + "name": "id", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "username", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecEnableAutoExpandingArchive": { + "post": { + "summary": "ExecEnableAutoExpandingArchive", + "tags": [ + "Email-Exchange > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ID": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecExchangeRoleRepair": { + "post": { + "summary": "ExecExchangeRoleRepair", + "tags": [ + "CIPP > Settings" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.AppSettings.ReadWrite", + "parameters": [ + { + "name": "tenantId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tenantId": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecExcludeLicenses": { + "post": { + "summary": "ExecExcludeLicenses", + "tags": [ + "CIPP > Settings" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.AppSettings.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Action": { + "type": "string" + }, + "FullReset": { + "type": "string" + }, + "GUID": { + "type": "string" + }, + "SKUName": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecExcludeTenant": { + "post": { + "summary": "ExecExcludeTenant", + "tags": [ + "CIPP > Settings" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.AppSettings.ReadWrite", + "parameters": [ + { + "name": "AddExclusion", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "List", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "ListAll", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "RemoveExclusion", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "value": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecExtensionMapping": { + "get": { + "summary": "ExecExtensionMapping", + "tags": [ + "CIPP > Extensions" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Extension.ReadWrite", + "parameters": [ + { + "name": "AddMapping", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "AutoMapping", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "List", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/ExecExtensionNinjaOneQueue": { + "get": { + "summary": "ExecExtensionNinjaOneQueue", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Extension.ReadWrite" + } + }, + "/api/ExecExtensionSync": { + "get": { + "summary": "ExecExtensionSync", + "tags": [ + "CIPP > Extensions" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Extension.ReadWrite", + "parameters": [ + { + "name": "Extension", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "TenantID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/ExecExtensionTest": { + "get": { + "summary": "ExecExtensionTest", + "tags": [ + "CIPP > Extensions" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Extension.Read", + "parameters": [ + { + "name": "extensionName", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/ExecExtensionsConfig": { + "post": { + "summary": "ExecExtensionsConfig", + "tags": [ + "CIPP > Extensions" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Extension.ReadWrite", + "x-cipp-dynamic-options": true, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Hudu": { + "type": "string" + }, + "NinjaOne": { + "type": "string" + }, + "PSObject": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecFeatureFlag": { + "post": { + "summary": "ExecFeatureFlag", + "tags": [ + "CIPP > Core" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.AppSettings.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Action": { + "type": "string" + }, + "Enabled": { + "type": "boolean" + }, + "Id": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecGDAPAccessAssignment": { + "patch": { + "summary": "ExecGDAPAccessAssignment", + "tags": [ + "Tenant > GDAP" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Relationship.ReadWrite", + "parameters": [ + { + "name": "Action", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Id", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Action": { + "type": "string" + }, + "Id": { + "type": "string" + }, + "RoleTemplateId": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecGDAPInvite": { + "delete": { + "summary": "ExecGDAPInvite", + "tags": [ + "Tenant > GDAP" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Relationship.ReadWrite", + "parameters": [ + { + "name": "Action", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Action": { + "type": "string" + }, + "gdapTemplate": { + "$ref": "#/components/schemas/LabelValue" + }, + "inviteCount": { + "type": "number" + }, + "InviteId": { + "type": "string" + }, + "Reference": { + "type": "string" + }, + "roleMappings": { + "$ref": "#/components/schemas/LabelValue" + }, + "roleDefinitionId": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecGDAPInviteApproved": { + "get": { + "summary": "ExecGDAPInviteApproved", + "tags": [ + "Tenant > GDAP" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Relationship.ReadWrite" + } + }, + "/api/ExecGDAPRemoveGArole": { + "post": { + "summary": "ExecGDAPRemoveGArole", + "tags": [ + "Tenant > GDAP" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Relationship.ReadWrite", + "parameters": [ + { + "name": "GDAPId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "GDAPId": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecGDAPRoleTemplate": { + "delete": { + "summary": "ExecGDAPRoleTemplate", + "tags": [ + "Tenant > GDAP" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Relationship.ReadWrite", + "x-cipp-dynamic-options": true, + "parameters": [ + { + "name": "Action", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "TemplateId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "gdapTemplate": { + "$ref": "#/components/schemas/LabelValue" + }, + "GroupId": { + "type": "string" + }, + "inviteCount": { + "type": "number" + }, + "OriginalTemplateId": { + "type": "string" + }, + "Reference": { + "type": "string" + }, + "RoleMappings": { + "$ref": "#/components/schemas/LabelValue" + }, + "TemplateId": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecGDAPTrace": { + "get": { + "summary": "Tests the complete GDAP (Granular Delegated Admin Privileges) access path for a user.", + "tags": [ + "CIPP > Settings" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "GDAP Access Path Testing:\n 1. Validates input parameters (TenantFilter and UPN)\n 2. Retrieves customer tenant information\n 3. Gets all active GDAP relationships for the customer tenant\n 4. Locates the UPN in the partner tenant\n 5. Gets user's transitive group memberships (handles nested groups automatically)\n 6. For each GDAP relationship:\n - Retrieves all access assignments (mapped security groups)\n - For each group: checks user membership (direct or nested) and traces the path\n - Maps roles to relationships and groups\n 7. For each of the 15 GDAP roles:\n - Finds all relationships/groups that have this role assigned\n - Checks if user is a member of any group with this role\n - Builds complete access path showing how user gets the role (if they do)\n 8. Returns comprehensive JSON with role-centric view and complete path traces", + "x-cipp-role": "CIPP.AppSettings.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "UPN", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/ExecGeoIPLookup": { + "post": { + "summary": "ExecGeoIPLookup", + "tags": [ + "CIPP > Core" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Core.Read", + "parameters": [ + { + "name": "IP", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "IP": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecGetLocalAdminPassword": { + "post": { + "summary": "ExecGetLocalAdminPassword", + "tags": [ + "Endpoint > MEM" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.Device.Read", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "guid": { + "type": "string" + }, + "TenantFilter": { + "type": "string" + } + }, + "required": [ + "TenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecGetRecoveryKey": { + "post": { + "summary": "ExecGetRecoveryKey", + "tags": [ + "Endpoint > MEM" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.Device.Read", + "parameters": [ + { + "name": "GUID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "GUID": { + "type": "string" + }, + "RecoveryKeyType": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecGitHubAction": { + "post": { + "summary": "Invoke GitHub Action", + "tags": [ + "Tools > GitHub" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Call GitHub API\n .ROLE\n CIPP.Extension.ReadWrite\n .FUNCTIONALITY\n Entrypoint,AnyTenant\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", + "x-cipp-role": "CIPP.Extension.ReadWrite", + "parameters": [ + { + "name": "Action", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Action": { + "type": "string" + }, + "Description": { + "type": "string" + }, + "includeforks": { + "type": "boolean" + }, + "orgName": { + "$ref": "#/components/schemas/LabelValue" + }, + "policySource": { + "$ref": "#/components/schemas/LabelValue" + }, + "Private": { + "type": "boolean" + }, + "repoName": { + "type": "string" + }, + "searchTerm": { + "$ref": "#/components/schemas/LabelValue" + } + } + } + } + } + } + } + }, + "/api/ExecGraphExplorerPreset": { + "delete": { + "summary": "ExecGraphExplorerPreset", + "tags": [ + "Tenant > Tools" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Core.Read", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "action": { + "type": "string" + }, + "AsApp": { + "type": "boolean" + }, + "endpoint": { + "type": "string" + }, + "IsShared": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "NoPagination": { + "type": "boolean" + }, + "preset": { + "type": "string" + }, + "reportTemplate": { + "$ref": "#/components/schemas/LabelValue" + }, + "ReverseTenantLookup": { + "type": "boolean" + }, + "ReverseTenantLookupProperty": { + "type": "string" + }, + "version": { + "$ref": "#/components/schemas/LabelValue" + } + } + } + } + } + } + } + }, + "/api/ExecGroupsDelete": { + "post": { + "summary": "ExecGroupsDelete", + "tags": [ + "Email-Exchange > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "parameters": [ + { + "name": "displayName", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "GroupType", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "displayName": { + "type": "string" + }, + "GroupType": { + "type": "string" + }, + "id": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecGroupsDeliveryManagement": { + "post": { + "summary": "ExecGroupsDeliveryManagement", + "tags": [ + "Email-Exchange > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Group.ReadWrite", + "parameters": [ + { + "name": "GroupType", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "ID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "OnlyAllowInternal", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "GroupType": { + "type": "string" + }, + "ID": { + "type": "string" + }, + "OnlyAllowInternal": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecGroupsHideFromGAL": { + "post": { + "summary": "ExecGroupsHideFromGAL", + "tags": [ + "Email-Exchange > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Group.ReadWrite", + "parameters": [ + { + "name": "GroupType", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "HideFromGAL", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "ID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "GroupType": { + "type": "string" + }, + "HideFromGAL": { + "type": "string" + }, + "ID": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecHVEUser": { + "post": { + "summary": "ExecHVEUser", + "tags": [ + "Email-Exchange > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "displayName": { + "type": "string" + }, + "password": { + "type": "string" + }, + "primarySMTPAddress": { + "type": "string" + }, + "TenantFilter": { + "type": "string" + } + }, + "required": [ + "TenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecHideFromGAL": { + "post": { + "summary": "ExecHideFromGAL", + "tags": [ + "Email-Exchange > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "parameters": [ + { + "name": "HideFromGAL", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "ID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "HideFromGAL": { + "type": "string" + }, + "ID": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecIncidentsList": { + "get": { + "summary": "ExecIncidentsList", + "tags": [ + "Security" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Security.Incident.Read", + "parameters": [ + { + "name": "EndDate", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "StartDate", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ExecJITAdmin": { + "post": { + "summary": "ExecJITAdmin", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Just-in-time admin management API endpoint. This function can create users, add roles, remove roles, delete, or disable a user.\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", + "x-cipp-role": "Identity.Role.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "GroupMemberships": { + "type": "string" + }, + "useGroups": { + "type": "boolean" + }, + "useRoles": { + "type": "boolean" + }, + "AdminRoles": { + "$ref": "#/components/schemas/LabelValue" + }, + "Domain": { + "$ref": "#/components/schemas/LabelValue" + }, + "EndDate": { + "type": "string", + "format": "date-time" + }, + "existingUser": { + "$ref": "#/components/schemas/LabelValue" + }, + "ExpireAction": { + "$ref": "#/components/schemas/LabelValue" + }, + "FirstName": { + "type": "string" + }, + "jitAdminTemplate": { + "$ref": "#/components/schemas/LabelValue" + }, + "LastName": { + "type": "string" + }, + "PostExecution": { + "type": "array", + "items": { + "type": "string" + } + }, + "StartDate": { + "type": "string", + "format": "date-time" + }, + "tenantFilter": { + "type": "string" + }, + "userAction": { + "type": "string", + "enum": [ + "create", + "select" + ] + }, + "UseTAP": { + "type": "boolean" + }, + "Username": { + "type": "string" + }, + "Reason": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecJITAdminSettings": { + "post": { + "summary": "ExecJITAdminSettings", + "tags": [ + "CIPP > Settings" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.AppSettings.ReadWrite", + "parameters": [ + { + "name": "Action", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Action": { + "type": "string" + }, + "MaxDuration": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecLicenseSearch": { + "post": { + "summary": "ExecLicenseSearch", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Core.Read", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "skuIds": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecListAppId": { + "get": { + "summary": "ExecListAppId", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Core.ReadWrite" + }, + "post": { + "summary": "ExecListAppId", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Core.ReadWrite" + } + }, + "/api/ExecListBackup": { + "get": { + "summary": "ExecListBackup", + "tags": [ + "CIPP > Core" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Backup.Read", + "parameters": [ + { + "name": "BackupName", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "NameOnly", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "Type", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/ExecLogRetentionConfig": { + "post": { + "summary": "ExecLogRetentionConfig", + "tags": [ + "CIPP > Settings" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.AppSettings.ReadWrite", + "parameters": [ + { + "name": "List", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "RetentionDays": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecMailTest": { + "get": { + "summary": "ExecMailTest", + "tags": [ + "Email-Exchange > Tools" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Core.Read", + "parameters": [ + { + "name": "Action", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/ExecMailboxMobileDevices": { + "get": { + "summary": "ExecMailboxMobileDevices", + "tags": [ + "Email-Exchange > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "parameters": [ + { + "name": "Delete", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "deviceid", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "guid", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Quarantine", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "Userid", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/ExecMailboxRestore": { + "post": { + "summary": "ExecMailboxRestore", + "tags": [ + "Email-Exchange > Tools" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "parameters": [ + { + "name": "Action", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Identity", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "TenantFilter": { + "type": "string" + }, + "RequestName": { + "type": "string" + }, + "SourceMailbox": { + "type": "string" + }, + "TargetMailbox": { + "type": "string" + }, + "BadItemLimit": { + "type": "integer" + }, + "LargeItemLimit": { + "type": "integer" + }, + "AcceptLargeDataLoss": { + "type": "boolean" + }, + "AssociatedMessagesCopyOption": { + "type": "string" + }, + "ExcludeFolders": { + "type": "array", + "items": { + "type": "string" + } + }, + "IncludeFolders": { + "type": "array", + "items": { + "type": "string" + } + }, + "BatchName": { + "type": "string" + }, + "CompletedRequestAgeLimit": { + "type": "integer" + }, + "ConflictResolutionOption": { + "type": "string" + }, + "SourceRootFolder": { + "type": "string" + }, + "TargetRootFolder": { + "type": "string" + }, + "TargetType": { + "type": "string" + }, + "ExcludeDumpster": { + "type": "boolean" + }, + "SourceIsArchive": { + "type": "boolean" + }, + "TargetIsArchive": { + "type": "boolean" + }, + "Action": { + "type": "string" + }, + "Identity": { + "type": "string" + } + }, + "required": [ + "TenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecMaintenanceScripts": { + "get": { + "summary": "ExecMaintenanceScripts", + "tags": [ + "CIPP > Settings" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.AppSettings.Read", + "parameters": [ + { + "name": "MakeLink", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "ScriptFile", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/ExecManageRetentionPolicies": { + "delete": { + "summary": "ExecManageRetentionPolicies", + "tags": [ + "Email-Exchange > Administration > Mailbox Retention" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.RetentionPolicies.ReadWrite", + "x-cipp-warnings": [ + "customDataformatter is present on the CippFormPage for this endpoint. Form field names and/or types may be renamed or restructured before POST. Verify the transformer function in the frontend page file and correct param names/types accordingly." + ], + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "name", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tenantFilter": { + "type": "string" + }, + "CreatePolicies": { + "type": "array", + "items": { + "type": "string" + } + }, + "ModifyPolicies": { + "type": "array", + "items": { + "type": "string" + } + }, + "DeletePolicies": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecManageRetentionTags": { + "delete": { + "summary": "ExecManageRetentionTags", + "tags": [ + "Email-Exchange > Administration > Mailbox Retention" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.RetentionPolicies.ReadWrite", + "x-cipp-warnings": [ + "customDataformatter is present on the CippFormPage for this endpoint. Form field names and/or types may be renamed or restructured before POST. Verify the transformer function in the frontend page file and correct param names/types accordingly." + ], + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "name", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Comment": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "CreateTags": { + "type": "array", + "items": { + "type": "string" + } + }, + "ModifyTags": { + "type": "array", + "items": { + "type": "string" + } + }, + "DeleteTags": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecMdoAlertsList": { + "get": { + "summary": "ExecMdoAlertsList", + "tags": [ + "Security" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Security.Alert.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ExecModifyCalPerms": { + "post": { + "summary": "ExecModifyCalPerms", + "tags": [ + "Email-Exchange > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "permissions": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "userID": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecModifyContactPerms": { + "post": { + "summary": "ExecModifyContactPerms", + "tags": [ + "Email-Exchange > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "permissions": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "userID": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecModifyMBPerms": { + "post": { + "summary": "ExecModifyMBPerms", + "tags": [ + "Email-Exchange > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true, + "properties": { + "mailboxRequests": { + "type": "string" + }, + "permissions": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "userID": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + }, + { + "type": "object", + "additionalProperties": true, + "properties": { + "mailboxRequests": { + "type": "string" + }, + "permissions": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "userID": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + ], + "description": "Accepts a single object or an array of objects. Each object must include the required fields." + } + } + } + } + } + }, + "/api/ExecNamedLocation": { + "post": { + "summary": "ExecNamedLocation", + "tags": [ + "Tenant > Conditional" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.ConditionalAccess.ReadWrite", + "parameters": [ + { + "name": "change", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "input", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "namedLocationId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "change": { + "type": "string" + }, + "input": { + "type": "string" + }, + "namedLocationId": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecNewSafeLinksPolicy": { + "post": { + "summary": "ExecNewSafeLinksPolicy", + "tags": [ + "Security > Safe-Links-Policy" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "This function creates a new Safe Links policy and an associated rule.\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", + "x-cipp-role": "Exchange.SpamFilter.ReadWrite", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tenantFilter": { + "type": "string" + }, + "PolicyName": { + "type": "string" + }, + "RuleName": { + "type": "string" + }, + "EnableSafeLinksForEmail": { + "type": "boolean" + }, + "EnableSafeLinksForTeams": { + "type": "boolean" + }, + "EnableSafeLinksForOffice": { + "type": "boolean" + }, + "TrackClicks": { + "type": "boolean" + }, + "AllowClickThrough": { + "type": "boolean" + }, + "ScanUrls": { + "type": "boolean" + }, + "EnableForInternalSenders": { + "type": "boolean" + }, + "DeliverMessageAfterScan": { + "type": "boolean" + }, + "DisableUrlRewrite": { + "type": "boolean" + }, + "EnableOrganizationBranding": { + "type": "boolean" + }, + "AdminDisplayName": { + "type": "string" + }, + "CustomNotificationText": { + "type": "string" + }, + "DoNotRewriteUrls": { + "type": "array", + "items": { + "type": "string" + } + }, + "Priority": { + "type": "integer" + }, + "Comments": { + "type": "string" + }, + "State": { + "type": "boolean" + }, + "SentTo": { + "type": "array", + "items": { + "type": "string" + } + }, + "SentToMemberOf": { + "type": "array", + "items": { + "type": "string" + } + }, + "RecipientDomainIs": { + "type": "array", + "items": { + "type": "string" + } + }, + "ExceptIfSentTo": { + "type": "array", + "items": { + "type": "string" + } + }, + "ExceptIfSentToMemberOf": { + "type": "array", + "items": { + "type": "string" + } + }, + "ExceptIfRecipientDomainIs": { + "type": "array", + "items": { + "type": "string" + } + }, + "Count": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecNotificationConfig": { + "post": { + "summary": "ExecNotificationConfig", + "tags": [ + "CIPP > Settings" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.AppSettings.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "webhook": { + "type": "string" + }, + "logsToInclude": { + "$ref": "#/components/schemas/LabelValue" + }, + "Severity": { + "$ref": "#/components/schemas/LabelValue" + }, + "onePerTenant": { + "type": "boolean" + }, + "sendtoIntegration": { + "type": "boolean" + } + } + } + } + } + } + } + }, + "/api/ExecOffboardTenant": { + "patch": { + "summary": "ExecOffboardTenant", + "tags": [ + "Tenant > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Administration.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "RemoveCSPGuestUsers": { + "type": "boolean" + }, + "RemoveCSPnotificationContacts": { + "type": "boolean" + }, + "RemoveDomainAnalyserData": { + "type": "boolean" + }, + "RemoveMultitenantCSPApps": { + "type": "boolean" + }, + "TenantFilter": { + "$ref": "#/components/schemas/LabelValue" + }, + "TerminateContract": { + "type": "boolean" + }, + "TerminateGDAP": { + "type": "boolean" + }, + "vendorApplications": { + "$ref": "#/components/schemas/LabelValue" + } + }, + "required": [ + "TenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecOffboardUser": { + "post": { + "summary": "ExecOffboardUser", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Offboard a user with configurable options for mailbox conversion, access delegation, license removal, and more", + "x-cipp-role": "Identity.User.ReadWrite", + "x-cipp-scheduled-branch": true, + "x-cipp-dynamic-options": true, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tenantFilter": { + "type": "string" + }, + "user": { + "type": "array", + "items": { + "type": "string" + } + }, + "ConvertToShared": { + "type": "boolean" + }, + "HideFromGAL": { + "type": "boolean" + }, + "removeCalendarInvites": { + "type": "boolean" + }, + "removePermissions": { + "type": "boolean" + }, + "removeCalendarPermissions": { + "type": "boolean" + }, + "RemoveRules": { + "type": "boolean" + }, + "RemoveMobile": { + "type": "boolean" + }, + "RemoveGroups": { + "type": "boolean" + }, + "RemoveLicenses": { + "type": "boolean" + }, + "RevokeSessions": { + "type": "boolean" + }, + "DisableSignIn": { + "type": "boolean" + }, + "ClearImmutableId": { + "type": "boolean" + }, + "ResetPass": { + "type": "boolean" + }, + "RemoveMFADevices": { + "type": "boolean" + }, + "RemoveTeamsPhoneDID": { + "type": "boolean" + }, + "DeleteUser": { + "type": "boolean" + }, + "AccessNoAutomap": { + "$ref": "#/components/schemas/LabelValue" + }, + "AccessAutomap": { + "$ref": "#/components/schemas/LabelValue" + }, + "OnedriveAccess": { + "$ref": "#/components/schemas/LabelValue" + }, + "forward": { + "type": "string" + }, + "disableForwarding": { + "type": "boolean" + }, + "KeepCopy": { + "type": "boolean" + }, + "OOO": { + "type": "string" + }, + "Scheduled": { + "$ref": "#/components/schemas/ScheduledTask" + }, + "PostExecution": { + "$ref": "#/components/schemas/PostExecution" + }, + "reference": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecOffloadFunctions": { + "post": { + "summary": "ExecOffloadFunctions", + "tags": [ + "CIPP > Settings" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.SuperAdmin.ReadWrite", + "parameters": [ + { + "name": "Action", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "OffloadFunctions": { + "type": "boolean" + } + } + } + } + } + } + } + }, + "/api/ExecOnboardTenant": { + "post": { + "summary": "ExecOnboardTenant", + "tags": [ + "Tenant > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Administration.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "addMissingGroups": { + "type": "string" + }, + "autoMapRoles": { + "type": "string" + }, + "Cancel": { + "type": "string" + }, + "gdapRoles": { + "$ref": "#/components/schemas/LabelValue" + }, + "id": { + "$ref": "#/components/schemas/LabelValue" + }, + "ignoreMissingRoles": { + "type": "boolean" + }, + "remapRoles": { + "type": "string" + }, + "Retry": { + "type": "string" + }, + "standardsExcludeAllTenants": { + "type": "boolean" + } + } + } + } + } + } + } + }, + "/api/ExecOneDriveShortCut": { + "post": { + "summary": "ExecOneDriveShortCut", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Identity.User.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "siteUrl": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "userid": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecOnedriveProvision": { + "post": { + "summary": "ExecOnedriveProvision", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Identity.User.ReadWrite", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "UserPrincipalName", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tenantFilter": { + "type": "string" + }, + "UserPrincipalName": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecPartnerMode": { + "post": { + "summary": "ExecPartnerMode", + "tags": [ + "CIPP > Settings" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.SuperAdmin.ReadWrite", + "parameters": [ + { + "name": "action", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "TenantMode": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecPartnerWebhook": { + "post": { + "summary": "ExecPartnerWebhook", + "tags": [ + "CIPP > Core" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.AppSettings.ReadWrite", + "parameters": [ + { + "name": "Action", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "CorrelationId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "EventType": { + "$ref": "#/components/schemas/LabelValue" + }, + "standardsExcludeAllTenants": { + "type": "boolean" + } + } + } + } + } + } + } + }, + "/api/ExecPasswordConfig": { + "post": { + "summary": "ExecPasswordConfig", + "tags": [ + "CIPP > Settings" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.AppSettings.ReadWrite", + "parameters": [ + { + "name": "List", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "appendNumber": { + "type": "string" + }, + "appendSpecialChar": { + "type": "string" + }, + "capitalizeWords": { + "type": "string" + }, + "charCount": { + "type": "string" + }, + "includeDigits": { + "type": "string" + }, + "includeLowercase": { + "type": "string" + }, + "includeSpecialChars": { + "type": "string" + }, + "includeUppercase": { + "type": "string" + }, + "passwordType": { + "type": "string" + }, + "separator": { + "type": "string" + }, + "specialCharSet": { + "type": "string" + }, + "wordCount": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecPasswordNeverExpires": { + "post": { + "summary": "ExecPasswordNeverExpires", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Identity.User.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "PasswordPolicy": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "userId": { + "type": "string" + }, + "userPrincipalName": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecPerUserMFA": { + "post": { + "summary": "ExecPerUserMFA", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Identity.User.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "State": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "userId": { + "type": "string" + }, + "userPrincipalName": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecPermissionRepair": { + "get": { + "summary": "This endpoint will update the CIPP-SAM app permissions.", + "tags": [ + "CIPP > Settings" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Merges new permissions from the SAM manifest into the AppPermissions entry for CIPP-SAM.\n .FUNCTIONALITY\n Entrypoint\n .ROLE\n CIPP.AppSettings.ReadWrite\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", + "x-cipp-role": "CIPP.AppSettings.ReadWrite" + } + }, + "/api/ExecQuarantineManagement": { + "post": { + "summary": "ExecQuarantineManagement", + "tags": [ + "Email-Exchange > Spamfilter" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.SpamFilter.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "AllowSender": { + "type": "string" + }, + "Identity": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "Type": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecRemoveMailboxRule": { + "post": { + "summary": "ExecRemoveMailboxRule", + "tags": [ + "Email-Exchange > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "parameters": [ + { + "name": "ruleId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "ruleName", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "userPrincipalName", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ruleId": { + "type": "string" + }, + "ruleName": { + "type": "string" + }, + "TenantFilter": { + "type": "string" + }, + "userPrincipalName": { + "type": "string" + } + }, + "required": [ + "TenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecRemoveRestrictedUser": { + "post": { + "summary": "ExecRemoveRestrictedUser", + "tags": [ + "Email-Exchange > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Removes a user from the restricted senders list in Exchange Online.\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", + "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "SenderAddress": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecRemoveTeamsVoicePhoneNumberAssignment": { + "post": { + "summary": "ExecRemoveTeamsVoicePhoneNumberAssignment", + "tags": [ + "Teams-Sharepoint" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Teams.Voice.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "AssignedTo": { + "type": "string" + }, + "PhoneNumber": { + "type": "string" + }, + "PhoneNumberType": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecRemoveTenant": { + "post": { + "summary": "ExecRemoveTenant", + "tags": [ + "CIPP > Settings" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Administration.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "TenantID": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecRenameAPDevice": { + "post": { + "summary": "ExecRenameAPDevice", + "tags": [ + "Endpoint > Autopilot" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.Autopilot.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "deviceId": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "serialNumber": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecReprocessUserLicenses": { + "post": { + "summary": "ExecReprocessUserLicenses", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Identity.User.ReadWrite", + "parameters": [ + { + "name": "ID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "userPrincipalName", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ID": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "userPrincipalName": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecResetMFA": { + "post": { + "summary": "ExecResetMFA", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Identity.User.ReadWrite", + "parameters": [ + { + "name": "ID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ID": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecResetPass": { + "post": { + "summary": "ExecResetPass", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Identity.User.ReadWrite", + "parameters": [ + { + "name": "displayName", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "ID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "MustChange", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "displayName": { + "type": "string" + }, + "ID": { + "type": "string" + }, + "MustChange": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecRestoreBackup": { + "post": { + "summary": "ExecRestoreBackup", + "tags": [ + "CIPP > Settings" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.AppSettings.ReadWrite", + "x-cipp-dynamic-options": true, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "BackupName": { + "type": "string" + }, + "SelectedTypes": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecRestoreDeleted": { + "post": { + "summary": "ExecRestoreDeleted", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Directory.ReadWrite", + "parameters": [ + { + "name": "ID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "displayName": { + "type": "string" + }, + "ID": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "userPrincipalName": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecRevokeSessions": { + "post": { + "summary": "ExecRevokeSessions", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Identity.User.ReadWrite", + "parameters": [ + { + "name": "id", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "Username", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "Username": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecRunBackup": { + "get": { + "summary": "ExecRunBackup", + "tags": [ + "CIPP > Settings" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.AppSettings.ReadWrite" + } + }, + "/api/ExecRunTenantGroupRule": { + "post": { + "summary": "Execute tenant group dynamic rules immediately", + "tags": [ + "CIPP > Settings" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "This function executes dynamic tenant group rules for immediate membership updates\n .FUNCTIONALITY\n Entrypoint,AnyTenant\n .ROLE\n Tenant.Groups.ReadWrite\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", + "x-cipp-role": "Tenant.Groups.ReadWrite", + "parameters": [ + { + "name": "groupId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "groupId": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecSAMAppPermissions": { + "post": { + "summary": "ExecSAMAppPermissions", + "tags": [ + "CIPP > Settings" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.SuperAdmin.ReadWrite", + "parameters": [ + { + "name": "Action", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Permissions": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecSAMRoles": { + "post": { + "summary": "ExecSAMRoles", + "tags": [ + "CIPP > Settings" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.SuperAdmin.ReadWrite", + "parameters": [ + { + "name": "Action", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Roles": { + "type": "string" + }, + "Tenants": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecSAMSetup": { + "post": { + "summary": "ExecSAMSetup", + "tags": [ + "CIPP > Setup" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.AppSettings.ReadWrite", + "parameters": [ + { + "name": "CheckSetupProcess", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "code", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "count", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "CreateSAM", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "error", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "error_description", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "step", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "applicationid": { + "type": "string" + }, + "applicationsecret": { + "type": "string" + }, + "RefreshToken": { + "type": "string" + }, + "setkeys": { + "type": "string" + }, + "tenantid": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecScheduleMailboxVacation": { + "post": { + "summary": "ExecScheduleMailboxVacation", + "tags": [ + "Email-Exchange > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "autoMap": { + "type": "string" + }, + "calendarPermission": { + "type": "string" + }, + "canViewPrivateItems": { + "type": "string" + }, + "delegates": { + "type": "string" + }, + "endDate": { + "type": "string" + }, + "includeCalendar": { + "type": "string" + }, + "mailboxOwners": { + "type": "string" + }, + "permissionTypes": { + "type": "string" + }, + "postExecution": { + "$ref": "#/components/schemas/PostExecution" + }, + "reference": { + "type": "string" + }, + "startDate": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecScheduleOOOVacation": { + "post": { + "summary": "ExecScheduleOOOVacation", + "tags": [ + "Email-Exchange > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "endDate": { + "type": "string" + }, + "externalMessage": { + "type": "string" + }, + "internalMessage": { + "type": "string" + }, + "postExecution": { + "$ref": "#/components/schemas/PostExecution" + }, + "reference": { + "type": "string" + }, + "startDate": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "Users": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecSchedulerBillingRun": { + "get": { + "summary": "ExecSchedulerBillingRun", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Scheduler.Billing.ReadWrite" + } + }, + "/api/ExecSendOrgMessage": { + "get": { + "summary": "ExecSendOrgMessage", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Directory.ReadWrite", + "parameters": [ + { + "name": "freq", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "ID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "type", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "URL", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/ExecSendPush": { + "post": { + "summary": "ExecSendPush", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Identity.User.Read", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "TenantFilter": { + "type": "string" + }, + "UserEmail": { + "type": "string" + } + }, + "required": [ + "TenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecServicePrincipals": { + "get": { + "summary": "ExecServicePrincipals", + "tags": [ + "CIPP > Core" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Application.ReadWrite", + "parameters": [ + { + "name": "Action", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "AppId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Id", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Select", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/ExecSetAPDeviceGroupTag": { + "post": { + "summary": "ExecSetAPDeviceGroupTag", + "tags": [ + "Endpoint > Autopilot" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.Autopilot.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "deviceId": { + "type": "string" + }, + "groupTag": { + "type": "string" + }, + "serialNumber": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecSetCIPPAutoBackup": { + "post": { + "summary": "ExecSetCIPPAutoBackup", + "tags": [ + "CIPP > Core" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Backup.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Enabled": { + "type": "boolean" + } + } + } + } + } + } + } + }, + "/api/ExecSetCalendarProcessing": { + "post": { + "summary": "ExecSetCalendarProcessing", + "tags": [ + "Email-Exchange > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "additionalResponse": { + "type": "string" + }, + "addOrganizerToSubject": { + "type": "string" + }, + "allowConflicts": { + "type": "string" + }, + "allowRecurringMeetings": { + "type": "string" + }, + "automaticallyAccept": { + "type": "string" + }, + "automaticallyProcess": { + "type": "string" + }, + "bookingWindowInDays": { + "type": "string" + }, + "deleteComments": { + "type": "string" + }, + "deleteSubject": { + "type": "string" + }, + "maxConflicts": { + "type": "string" + }, + "maximumDurationInMinutes": { + "type": "string" + }, + "minimumDurationInMinutes": { + "type": "string" + }, + "processExternalMeetingMessages": { + "type": "string" + }, + "removeCanceledMeetings": { + "type": "string" + }, + "removeOldMeetingMessages": { + "type": "string" + }, + "removePrivateProperty": { + "type": "string" + }, + "scheduleOnlyDuringWorkHours": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "UPN": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecSetCloudManaged": { + "post": { + "summary": "ExecSetCloudManaged", + "tags": [ + "Identity" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Sets the cloud-managed status of a user, group, or contact.\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", + "x-cipp-role": "Identity.DirSync.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "displayName": { + "type": "string" + }, + "ID": { + "type": "string" + }, + "isCloudManaged": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecSetLitigationHold": { + "post": { + "summary": "ExecSetLitigationHold", + "tags": [ + "Email-Exchange > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "days": { + "type": "string" + }, + "disable": { + "type": "string" + }, + "Identity": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "UPN": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecSetMailboxEmailSize": { + "post": { + "summary": "ExecSetMailboxEmailSize", + "tags": [ + "Email-Exchange > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "maxReceiveSize": { + "type": "string" + }, + "maxSendSize": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "UPN": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecSetMailboxLocale": { + "post": { + "summary": "ExecSetMailboxLocale", + "tags": [ + "Email-Exchange > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "locale": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "user": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecSetMailboxQuota": { + "post": { + "summary": "ExecSetMailboxQuota", + "tags": [ + "Email-Exchange > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "IssueWarningQuota": { + "type": "string" + }, + "ProhibitSendQuota": { + "type": "string" + }, + "ProhibitSendReceiveQuota": { + "type": "string" + }, + "quota": { + "type": "string" + }, + "tenantfilter": { + "type": "string" + }, + "user": { + "type": "string" + } + }, + "required": [ + "tenantfilter" + ] + } + } + } + } + } + }, + "/api/ExecSetMailboxRetentionPolicies": { + "post": { + "summary": "ExecSetMailboxRetentionPolicies", + "tags": [ + "Email-Exchange > Administration > Mailbox Retention" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Mailboxes": { + "type": "string" + }, + "PolicyName": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecSetMailboxRule": { + "post": { + "summary": "ExecSetMailboxRule", + "tags": [ + "Email-Exchange > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Disable": { + "type": "string" + }, + "Enable": { + "type": "string" + }, + "ruleId": { + "type": "string" + }, + "ruleName": { + "type": "string" + }, + "TenantFilter": { + "type": "string" + }, + "userPrincipalName": { + "type": "string" + } + }, + "required": [ + "TenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecSetMdoAlert": { + "post": { + "summary": "ExecSetMdoAlert", + "tags": [ + "Security" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Security.Incident.ReadWrite", + "parameters": [ + { + "name": "Assigned", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Classification", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Determination", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "GUID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Status", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Assigned": { + "type": "string" + }, + "Classification": { + "type": "string" + }, + "Determination": { + "type": "string" + }, + "GUID": { + "type": "string" + }, + "Status": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecSetOoO": { + "post": { + "summary": "ExecSetOoO", + "tags": [ + "Email-Exchange > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "AutoReplyState": { + "type": "string" + }, + "EndTime": { + "type": "string" + }, + "ExternalMessage": { + "type": "string" + }, + "input": { + "type": "string" + }, + "InternalMessage": { + "type": "string" + }, + "StartTime": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "userId": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecSetPackageTag": { + "post": { + "summary": "ExecSetPackageTag", + "tags": [ + "CIPP > Core" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Core.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "GUID": { + "type": "string" + }, + "Package": { + "type": "string" + }, + "Remove": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecSetRecipientLimits": { + "post": { + "summary": "ExecSetRecipientLimits", + "tags": [ + "Email-Exchange > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Identity": { + "type": "string" + }, + "recipientLimit": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "userid": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecSetRetentionHold": { + "post": { + "summary": "ExecSetRetentionHold", + "tags": [ + "Email-Exchange > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "disable": { + "type": "string" + }, + "Identity": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "UPN": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecSetSecurityAlert": { + "post": { + "summary": "ExecSetSecurityAlert", + "tags": [ + "Security" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Security.Alert.ReadWrite", + "parameters": [ + { + "name": "GUID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Provider", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Status", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "Vendor", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "GUID": { + "type": "string" + }, + "Provider": { + "type": "string" + }, + "Status": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "Vendor": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecSetSecurityIncident": { + "post": { + "summary": "ExecSetSecurityIncident", + "tags": [ + "Security" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Security.Incident.ReadWrite", + "parameters": [ + { + "name": "Assigned", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Classification", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Determination", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "GUID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Redirected", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Status", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Assigned": { + "type": "string" + }, + "Classification": { + "type": "string" + }, + "Determination": { + "type": "string" + }, + "GUID": { + "type": "string" + }, + "Redirected": { + "type": "string" + }, + "Status": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecSetSharePointMember": { + "post": { + "summary": "ExecSetSharePointMember", + "tags": [ + "Teams-Sharepoint" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Sharepoint.Site.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Add": { + "type": "string" + }, + "GroupID": { + "type": "string" + }, + "SharePointType": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "user": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecSetUserPhoto": { + "post": { + "summary": "ExecSetUserPhoto", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Identity.User.ReadWrite", + "parameters": [ + { + "name": "action", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "userId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "action": { + "type": "string" + }, + "photoData": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "userId": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecSharePointPerms": { + "post": { + "summary": "ExecSharePointPerms", + "tags": [ + "Teams-Sharepoint" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Sharepoint.Site.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "onedriveAccessUser": { + "type": "string" + }, + "RemovePermission": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "UPN": { + "type": "string" + }, + "URL": { + "type": "string" + }, + "user": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecStandardConvert": { + "get": { + "summary": "ExecStandardConvert", + "tags": [ + "Tenant > Standards" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Standards.ReadWrite", + "x-cipp-dynamic-options": true + } + }, + "/api/ExecStandardsRun": { + "get": { + "summary": "ExecStandardsRun", + "tags": [ + "Tenant > Standards" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Standards.ReadWrite", + "parameters": [ + { + "name": "templateId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ExecStartManagedFolderAssistant": { + "post": { + "summary": "ExecStartManagedFolderAssistant", + "tags": [ + "Email-Exchange > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "parameters": [ + { + "name": "Id", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Id": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "UserPrincipalName": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecSyncAPDevices": { + "post": { + "summary": "ExecSyncAPDevices", + "tags": [ + "Endpoint > Autopilot" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.Autopilot.ReadWrite", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecSyncDEP": { + "post": { + "summary": "ExecSyncDEP", + "tags": [ + "Endpoint > MEM" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Syncs devices from Apple Business Manager to Intune\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)\n $APIName = $Request.Params.CIPPEndpoint\n $Headers = $Request.Headers", + "x-cipp-role": "Endpoint.MEM.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecSyncVPP": { + "post": { + "summary": "ExecSyncVPP", + "tags": [ + "Endpoint > Applications" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.Application.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecTeamsVoicePhoneNumberAssignment": { + "post": { + "summary": "ExecTeamsVoicePhoneNumberAssignment", + "tags": [ + "Teams-Sharepoint" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Teams.Voice.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "input": { + "type": "string" + }, + "locationOnly": { + "type": "string" + }, + "PhoneNumber": { + "type": "string" + }, + "PhoneNumberType": { + "type": "string" + }, + "TenantFilter": { + "type": "string" + } + }, + "required": [ + "TenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecTenantGroup": { + "delete": { + "summary": "Entrypoint for tenant group management", + "tags": [ + "CIPP > Settings" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "This function is used to manage tenant groups in CIPP\n .FUNCTIONALITY\n Entrypoint,AnyTenant\n .ROLE\n Tenant.Groups.ReadWrite\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", + "x-cipp-role": "Tenant.Groups.ReadWrite", + "x-cipp-warnings": [ + "customDataformatter is present on the CippFormPage for this endpoint. Form field names and/or types may be renamed or restructured before POST. Verify the transformer function in the frontend page file and correct param names/types accordingly." + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Action": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "groupName": { + "type": "string" + }, + "groupDescription": { + "type": "string" + }, + "groupType": { + "type": "string", + "enum": [ + "static", + "dynamic" + ] + }, + "members": { + "type": "array", + "items": { + "type": "string" + } + }, + "dynamicRules": { + "type": "array", + "items": { + "type": "string" + } + }, + "ruleLogic": { + "type": "string" + }, + "value": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecTestRun": { + "post": { + "summary": "ExecTestRun", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Tests.ReadWrite", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecTimeSettings": { + "post": { + "summary": "ExecTimeSettings", + "tags": [ + "CIPP > Settings" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.SuperAdmin.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "BusinessHoursStart": { + "$ref": "#/components/schemas/LabelValue" + }, + "Timezone": { + "$ref": "#/components/schemas/LabelValue" + } + } + } + } + } + } + } + }, + "/api/ExecTokenExchange": { + "post": { + "summary": "ExecTokenExchange", + "tags": [ + "CIPP > Setup" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.AppSettings.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tenantId": { + "type": "string" + }, + "tokenRequest": { + "type": "string" + }, + "tokenUrl": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecUniversalSearch": { + "get": { + "summary": "ExecUniversalSearch", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Core.Read", + "parameters": [ + { + "name": "name", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/ExecUniversalSearchV2": { + "get": { + "summary": "ExecUniversalSearchV2", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Core.Read", + "parameters": [ + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "searchTerms", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "type", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/ExecUpdateDriftDeviation": { + "post": { + "summary": "ExecUpdateDriftDeviation", + "tags": [ + "Tenant > Standards" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Standards.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "deviations": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "RemoveDriftCustomization": { + "type": "string" + }, + "TenantFilter": { + "type": "string" + } + }, + "required": [ + "TenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecUpdateRefreshToken": { + "post": { + "summary": "ExecUpdateRefreshToken", + "tags": [ + "CIPP > Setup" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.AppSettings.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "refreshtoken": { + "type": "string" + }, + "tenantId": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecUpdateSecureScore": { + "post": { + "summary": "ExecUpdateSecureScore", + "tags": [ + "Tenant > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Administration.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ControlName": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "resolutionType": { + "type": "string" + }, + "TenantFilter": { + "type": "string" + }, + "vendorInformation": { + "type": "string" + } + }, + "required": [ + "TenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecUserBookmarks": { + "post": { + "summary": "ExecUserBookmarks", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Core.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "currentSettings": { + "type": "string" + }, + "user": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecUserSettings": { + "post": { + "summary": "ExecUserSettings", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Core.ReadWrite", + "x-cipp-dynamic-options": true, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "currentSettings": { + "type": "string" + }, + "user": { + "$ref": "#/components/schemas/LabelValue" + } + } + } + } + } + } + } + }, + "/api/ExecWebhookSubscriptions": { + "delete": { + "summary": "ExecWebhookSubscriptions", + "tags": [ + "CIPP > Settings" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Alert.ReadWrite", + "parameters": [ + { + "name": "Action", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "WebhookID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/GetCippAlerts": { + "get": { + "summary": "GetCippAlerts", + "tags": [ + "CIPP > Core" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Core.Read", + "parameters": [ + { + "name": "localversion", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/GetVersion": { + "get": { + "summary": "GetVersion", + "tags": [ + "CIPP > Core" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.AppSettings.Read", + "parameters": [ + { + "name": "LocalVersion", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/ListAPDevices": { + "get": { + "summary": "ListAPDevices", + "tags": [ + "Endpoint > Autopilot" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.Autopilot.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListAdminPortalLicenses": { + "get": { + "summary": "ListAdminPortalLicenses", + "tags": [ + "CIPP > Core" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Core.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListAlertsQueue": { + "get": { + "summary": "ListAlertsQueue", + "tags": [ + "Tenant > Administration > Alerts" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Alert.Read", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "preset": { + "type": "string" + }, + "logbook": { + "type": "string" + }, + "Actions": { + "type": "string" + }, + "AlertComment": { + "type": "string" + }, + "command": { + "type": "string" + }, + "recurrence": { + "type": "string" + }, + "startDateTime": { + "type": "string" + }, + "postExecution": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ListAllTenantDeviceCompliance": { + "get": { + "summary": "ListAllTenantDeviceCompliance", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.DeviceCompliance.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListAntiPhishingFilters": { + "get": { + "summary": "ListAntiPhishingFilters", + "tags": [ + "Email-Exchange > Reports" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.SpamFilter.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListApiTest": { + "get": { + "summary": "ListApiTest", + "tags": [ + "CIPP > Core" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Core.Read" + } + }, + "/api/ListAppApprovalTemplates": { + "get": { + "summary": "ListAppApprovalTemplates", + "tags": [ + "Tenant > Administration > Application Approval" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Application.Read" + } + }, + "/api/ListAppConsentRequests": { + "get": { + "summary": "ListAppConsentRequests", + "tags": [ + "Tenant > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Administration.Read", + "parameters": [ + { + "name": "Filter", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "RequestStatus", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListAppProtectionPolicies": { + "get": { + "summary": "ListAppProtectionPolicies", + "tags": [ + "Endpoint > MEM" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.MEM.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListAppStatus": { + "get": { + "summary": "ListAppStatus", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.Device.Read", + "parameters": [ + { + "name": "AppFilter", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListApplicationQueue": { + "get": { + "summary": "ListApplicationQueue", + "tags": [ + "Endpoint > Applications" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.Application.Read" + } + }, + "/api/ListApps": { + "get": { + "summary": "ListApps", + "tags": [ + "Endpoint > Applications" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.Application.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListAppsRepository": { + "post": { + "summary": "ListAppsRepository", + "tags": [ + "Endpoint > Applications" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.Application.Read", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "AcceptLicense": { + "type": "boolean" + }, + "applicationName": { + "type": "string" + }, + "appType": { + "$ref": "#/components/schemas/LabelValue" + }, + "arch": { + "type": "boolean" + }, + "AssignTo": { + "type": "string", + "enum": [ + "On", + "allLicensedUsers", + "AllDevices", + "AllDevicesAndUsers", + "customGroup" + ] + }, + "customArguments": { + "type": "string" + }, + "customGroup": { + "type": "string" + }, + "customRepo": { + "type": "string" + }, + "customXml": { + "type": "string" + }, + "description": { + "type": "string" + }, + "detectionFile": { + "type": "string" + }, + "detectionPath": { + "type": "string" + }, + "DisableRestart": { + "type": "boolean" + }, + "displayName": { + "type": "string" + }, + "enforceSignatureCheck": { + "type": "boolean" + }, + "excludedApps": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LabelValue" + } + }, + "InstallAsSystem": { + "type": "boolean" + }, + "InstallationIntent": { + "type": "boolean" + }, + "installScript": { + "type": "string" + }, + "languages": { + "$ref": "#/components/schemas/LabelValue" + }, + "packagename": { + "type": "string" + }, + "packageSearch": { + "$ref": "#/components/schemas/LabelValue" + }, + "params": { + "type": "object", + "properties": { + "AccountKey": { + "type": "string" + }, + "dattoUrl": { + "type": "string" + }, + "Server": { + "type": "string" + } + } + }, + "publisher": { + "type": "string" + }, + "RemoveVersions": { + "type": "boolean" + }, + "rmmname": { + "$ref": "#/components/schemas/LabelValue" + }, + "runAs32Bit": { + "type": "boolean" + }, + "SharedComputerActivation": { + "type": "boolean" + }, + "uninstallScript": { + "type": "string" + }, + "updateChannel": { + "$ref": "#/components/schemas/LabelValue" + }, + "useCustomXml": { + "type": "boolean" + }, + "Search": { + "type": "string" + }, + "Repository": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ListAssignmentFilterTemplates": { + "get": { + "summary": "ListAssignmentFilterTemplates", + "tags": [ + "Endpoint > MEM" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.MEM.Read", + "parameters": [ + { + "name": "ID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/ListAssignmentFilters": { + "get": { + "summary": "ListAssignmentFilters", + "tags": [ + "Endpoint > MEM" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.MEM.Read", + "parameters": [ + { + "name": "filterId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListAuditLogSearches": { + "get": { + "summary": "ListAuditLogSearches", + "tags": [ + "Tenant > Administration > Alerts" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Alert.Read", + "parameters": [ + { + "name": "Days", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "SearchId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "Type", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/ListAuditLogTest": { + "get": { + "summary": "ListAuditLogTest", + "tags": [ + "Tenant > Administration > Alerts" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Alert.Read", + "parameters": [ + { + "name": "SearchId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListAuditLogs": { + "get": { + "summary": "ListAuditLogs", + "tags": [ + "Tenant > Administration > Alerts" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Alert.Read", + "parameters": [ + { + "name": "EndDate", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "LogId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "RelativeTime", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "StartDate", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListAutopilotconfig": { + "get": { + "summary": "ListAutopilotconfig", + "tags": [ + "Endpoint > Autopilot" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.Autopilot.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "type", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/ListAvailableTests": { + "get": { + "summary": "ListAvailableTests", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Dashboard.Read", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ListAzureADConnectStatus": { + "get": { + "summary": "ListAzureADConnectStatus", + "tags": [ + "Identity > Reports" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Directory.Read", + "parameters": [ + { + "name": "DataToReturn", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListBPA": { + "get": { + "summary": "ListBPA", + "tags": [ + "Tenant > Standards" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.BestPracticeAnalyser.Read", + "parameters": [ + { + "name": "Report", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListBPATemplates": { + "get": { + "summary": "ListBPATemplates", + "tags": [ + "Tenant > Standards" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.BestPracticeAnalyser.Read", + "parameters": [ + { + "name": "RawJson", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/ListBasicAuth": { + "get": { + "summary": "ListBasicAuth", + "tags": [ + "Identity > Reports" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Identity.AuditLog.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListBreachesAccount": { + "get": { + "summary": "ListBreachesAccount", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Core.Read", + "parameters": [ + { + "name": "account", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/ListBreachesTenant": { + "get": { + "summary": "ListBreachesTenant", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Core.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListCAtemplates": { + "get": { + "summary": "ListCAtemplates", + "tags": [ + "Tenant > Conditional" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.ConditionalAccess.Read", + "parameters": [ + { + "name": "GUID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "ID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/ListCSPLicenses": { + "get": { + "summary": "ListCSPLicenses", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Directory.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListCSPsku": { + "get": { + "summary": "ListCSPsku", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Directory.Read", + "parameters": [ + { + "name": "currentSkuOnly", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListCalendarPermissions": { + "get": { + "summary": "ListCalendarPermissions", + "tags": [ + "Email-Exchange > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Mailbox.Read", + "parameters": [ + { + "name": "ByUser", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "UseReportDB", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "UserID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/ListCheckExtAlerts": { + "get": { + "summary": "ListCheckExtAlerts", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Core.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListCommunityRepos": { + "get": { + "summary": "List community repositories in Table Storage", + "tags": [ + "Tools > GitHub" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "This function lists community repositories in Table Storage\n .FUNCTIONALITY\n Entrypoint,AnyTenant\n .ROLE\n CIPP.Core.Read\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", + "x-cipp-role": "CIPP.Core.Read", + "parameters": [ + { + "name": "WriteAccess", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/ListCompliancePolicies": { + "get": { + "summary": "ListCompliancePolicies", + "tags": [ + "Endpoint > MEM" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.MEM.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListConditionalAccessPolicies": { + "get": { + "summary": "ListConditionalAccessPolicies", + "tags": [ + "Tenant > Conditional" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.ConditionalAccess.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListConditionalAccessPolicyChanges": { + "get": { + "summary": "ListConditionalAccessPolicyChanges", + "tags": [ + "Tenant > Conditional" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.ConditionalAccess.Read", + "parameters": [ + { + "name": "displayName", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListConnectionFilter": { + "get": { + "summary": "ListConnectionFilter", + "tags": [ + "Email-Exchange > Spamfilter" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.ConnectionFilter.Read", + "x-cipp-dynamic-options": true, + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListConnectionFilterTemplates": { + "get": { + "summary": "ListConnectionFilterTemplates", + "tags": [ + "Email-Exchange > Spamfilter" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.ConnectionFilter.Read", + "parameters": [ + { + "name": "id", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/ListContactPermissions": { + "get": { + "summary": "ListContactPermissions", + "tags": [ + "Email-Exchange > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Mailbox.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "UserID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/ListContactTemplates": { + "get": { + "summary": "ListContactTemplates", + "tags": [ + "Email-Exchange > Administration > Contacts" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Contact.Read", + "parameters": [ + { + "name": "id", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/ListContacts": { + "get": { + "summary": "ListContacts", + "tags": [ + "Email-Exchange > Administration > Contacts" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Contact.Read", + "parameters": [ + { + "name": "id", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListCustomDataMappings": { + "get": { + "summary": "ListCustomDataMappings", + "tags": [ + "CIPP > Core" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Core.Read", + "parameters": [ + { + "name": "directoryObject", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "sourceType", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListCustomRole": { + "get": { + "summary": "ListCustomRole", + "tags": [ + "CIPP > Settings" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Core.Read" + } + }, + "/api/ListCustomVariables": { + "get": { + "summary": "ListCustomVariables", + "tags": [ + "CIPP > Settings" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Core.Read", + "parameters": [ + { + "name": "excludeGlobalReserved", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "includeSystem", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } }, - "/ListSharepointSettings": { + "/api/ListDBCache": { "get": { - "description": "ListSharepointSettings", - "summary": "ListSharepointSettings", - "tags": ["GET"], + "summary": "ListDBCache", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Core.Read", "parameters": [ { - "required": true, + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "type", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "TenantFilter", - "in": "query" + } + } + ] + } + }, + "/api/ListDefenderState": { + "get": { + "summary": "ListDefenderState", + "tags": [ + "Endpoint > MEM" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.MEM.Read", + "parameters": [ { - "required": true, + "name": "DeviceID", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "usertoGet", - "in": "query" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListDefenderTVM": { + "get": { + "summary": "ListDefenderTVM", + "tags": [ + "Endpoint > MEM" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.MEM.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListDeletedItems": { + "get": { + "summary": "ListDeletedItems", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Directory.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListDetectedAppDevices": { + "get": { + "summary": "ListDetectedAppDevices", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Identity.Device.Read", + "parameters": [ { - "required": true, + "name": "AppID", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "user", - "in": "query" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListDetectedApps": { + "get": { + "summary": "ListDetectedApps", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Identity.Device.Read", + "parameters": [ + { + "name": "DeviceID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "includeDevices", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListDeviceDetails": { + "get": { + "summary": "ListDeviceDetails", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Identity.Device.Read", + "parameters": [ + { + "name": "DeviceID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "DeviceName", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "DeviceSerial", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListDevices": { + "get": { + "summary": "ListDevices", + "tags": [ + "Endpoint > Reports" + ], + "security": [ + { + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } + }, + "x-cipp-role": "Endpoint.Device.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] } }, - "/ExecSendOrgMessage": { + "/api/ListDiagnosticsPresets": { "get": { - "description": "ExecSendOrgMessage", - "summary": "ExecSendOrgMessage", - "tags": ["GET"], - "parameters": [ + "summary": "ListDiagnosticsPresets", + "tags": [ + "CIPP > Core" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "ID", - "in": "query" + "400": { + "description": "Bad request — missing required field or invalid input" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "URL", - "in": "query" + "401": { + "description": "Unauthorized — invalid or missing bearer token" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "type", - "in": "query" + "403": { + "description": "Forbidden — caller lacks the required RBAC role" }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.SuperAdmin.Read" + } + }, + "/api/ListDirectoryObjects": { + "post": { + "summary": "ListDirectoryObjects", + "tags": [ + "CIPP > Core" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "freq", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Core.Read", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "asApp": { + "type": "string" + }, + "ids": { + "type": "string" + }, + "partnerLookup": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } } } } }, - "/AddSpamFilterTemplate": { - "post": { - "description": "AddSpamFilterTemplate", - "summary": "AddSpamFilterTemplate", - "tags": ["POST"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "PowerShellCommand", - "in": "body" - }, + "/api/ListDomainAnalyser": { + "get": { + "summary": "ListDomainAnalyser", + "tags": [ + "Tenant > Standards" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "name", - "in": "body" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } + }, + "x-cipp-role": "Tenant.DomainAnalyser.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] } }, - "/ListGroupTemplates": { + "/api/ListDomainHealth": { "get": { - "description": "ListGroupTemplates", - "summary": "ListGroupTemplates", - "tags": ["GET"], - "parameters": [ + "summary": "ListDomainHealth", + "tags": [ + "Tenant > Standards" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "id", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/ListAutopilotconfig": { - "get": { - "description": "ListAutopilotconfig", - "summary": "ListAutopilotconfig", - "tags": ["GET"], + }, + "x-cipp-role": "Tenant.DomainAnalyser.Read", "parameters": [ { - "required": true, + "name": "Action", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "TenantFilter", - "in": "query" + } }, { - "required": true, + "name": "Domain", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "type", - "in": "query" + } }, { - "required": true, + "name": "ExpectedInclude", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "UserID", - "in": "query" + } + }, + { + "name": "Record", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Selector", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Subdomains", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/ListDomains": { + "get": { + "summary": "ListDomains", + "tags": [ + "Tenant > Administration" + ], + "security": [ + { + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } + }, + "x-cipp-role": "Tenant.Administration.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] } }, - "/ExecSetSecurityIncident": { + "/api/ListEquipment": { "get": { - "description": "ExecSetSecurityIncident", - "summary": "ExecSetSecurityIncident", - "tags": ["GET"], - "parameters": [ + "summary": "ListEquipment", + "tags": [ + "Email-Exchange > Resources" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "Redirected", - "in": "query" + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "Determination", - "in": "query" + "400": { + "description": "Bad request — missing required field or invalid input" }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Equipment.Read", + "parameters": [ { - "required": true, + "name": "EquipmentId", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "TenantFilter", - "in": "query" + } }, { - "required": true, - "schema": { - "type": "string" - }, - "name": "Status", - "in": "query" + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListExConnectorTemplates": { + "get": { + "summary": "ListExConnectorTemplates", + "tags": [ + "Email-Exchange > Transport" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "Classification", - "in": "query" + "400": { + "description": "Bad request — missing required field or invalid input" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "GUID", - "in": "query" + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Connector.Read", + "parameters": [ { - "required": true, + "name": "id", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "Assigned", - "in": "query" + } + } + ] + } + }, + "/api/ListExchangeConnectors": { + "get": { + "summary": "ListExchangeConnectors", + "tags": [ + "Email-Exchange > Transport" + ], + "security": [ + { + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } + }, + "x-cipp-role": "Exchange.Connector.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] } }, - "/EditExConnector": { + "/api/ListExcludedLicenses": { "get": { - "description": "EditExConnector", - "summary": "EditExConnector", - "tags": ["GET"], - "parameters": [ + "summary": "ListExcludedLicenses", + "tags": [ + "CIPP > Settings" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "tenantfilter", - "in": "query" + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "guid", - "in": "query" + "400": { + "description": "Bad request — missing required field or invalid input" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "state", - "in": "query" + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.AppSettings.Read" + } + }, + "/api/ListExoRequest": { + "post": { + "summary": "ListExoRequest", + "tags": [ + "Email-Exchange > Tools" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "Type", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Core.Read", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Anchor": { + "type": "string" + }, + "AsApp": { + "type": "boolean" + }, + "AvailableCmdlets": { + "type": "string" + }, + "Cmdlet": { + "type": "string" + }, + "cmdParams": { + "type": "string" + }, + "Compliance": { + "type": "boolean" + }, + "Select": { + "type": "string" + }, + "TenantFilter": { + "type": "string" + }, + "UseSystemMailbox": { + "type": "string" + } + }, + "required": [ + "TenantFilter" + ] + } + } } } } }, - "/AddUser": { + "/api/ListExtensionCacheData": { "post": { - "description": "AddUser", - "summary": "AddUser", - "tags": ["POST"], - "parameters": [ + "summary": "List Extension Cache Data", + "tags": [ + "CIPP > Core" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "CopyFrom", - "in": "body" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/ListUserPhoto": { - "get": { - "description": "ListUserPhoto", - "summary": "ListUserPhoto", - "tags": ["GET"], + }, + "description": "This function is used to list the extension cache data.\n .FUNCTIONALITY\n Entrypoint\n .ROLE\n CIPP.Core.Read\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)\n $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter\n $DataTypes = $Request.Query.dataTypes -split ',' ?? $Request.Body.dataTypes ?? 'All'", + "x-cipp-role": "CIPP.Core.Read", "parameters": [ { - "required": true, + "name": "dataTypes", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "TenantFilter", - "in": "query" + } }, { - "required": true, - "schema": { - "type": "string" - }, - "name": "UserID", - "in": "query" + "$ref": "#/components/parameters/tenantFilter" } ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" - } + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "dataTypes": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] } - }, - "description": "Successful operation" + } } } } }, - "/ListConditionalAccessPolicies": { + "/api/ListExtensionSync": { "get": { - "description": "ListConditionalAccessPolicies", - "summary": "ListConditionalAccessPolicies", - "tags": ["GET"], - "parameters": [ + "summary": "ListExtensionSync", + "tags": [ + "CIPP > Extensions" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } + }, + "x-cipp-role": "CIPP.Extension.Read" } }, - "/ListBasicAuth": { + "/api/ListExtensionsConfig": { "get": { - "description": "ListBasicAuth", - "summary": "ListBasicAuth", - "tags": ["GET"], - "parameters": [ + "summary": "ListExtensionsConfig", + "tags": [ + "Uncategorized" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Extension.Read", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Description": { + "type": "string" + }, + "includeforks": { + "type": "boolean" + }, + "orgName": { + "$ref": "#/components/schemas/LabelValue" + }, + "Private": { + "type": "boolean" + }, + "repoName": { + "type": "string" + }, + "searchTerm": { + "$ref": "#/components/schemas/LabelValue" + } + } + } + } } } } }, - "/RemoveSpamfilterTemplate": { + "/api/ListExternalTenantInfo": { "get": { - "description": "RemoveSpamfilterTemplate", - "summary": "RemoveSpamfilterTemplate", - "tags": ["GET"], - "parameters": [ + "summary": "ListExternalTenantInfo", + "tags": [ + "Uncategorized" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "id", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/ExecGDAPInvite": { - "post": { - "description": "ExecGDAPInvite", - "summary": "ExecGDAPInvite", - "tags": ["POST"], + }, + "x-cipp-role": "CIPP.Core.Read", "parameters": [ { - "required": true, + "name": "tenant", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "gdapRoles", - "in": "body" + } + } + ] + } + }, + "/api/ListFeatureFlags": { + "get": { + "summary": "ListFeatureFlags", + "tags": [ + "CIPP > Core" + ], + "security": [ + { + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } + }, + "x-cipp-role": "CIPP.Core.Read" } }, - "/ListUserSigninLogs": { + "/api/ListFunctionParameters": { "get": { - "description": "ListUserSigninLogs", - "summary": "ListUserSigninLogs", - "tags": ["GET"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" - }, + "summary": "ListFunctionParameters", + "tags": [ + "Uncategorized" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "UserID", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/EditCAPolicy": { - "get": { - "description": "EditCAPolicy", - "summary": "EditCAPolicy", - "tags": ["GET"], + }, + "x-cipp-role": "CIPP.Core.Read", "parameters": [ { - "required": true, + "name": "Compliance", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "tenantFilter", - "in": "query" + } }, { - "required": true, + "name": "Function", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "guid", - "in": "query" + } }, { - "required": true, + "name": "Module", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "state", - "in": "query" + } + } + ] + } + }, + "/api/ListFunctionStats": { + "get": { + "summary": "ListFunctionStats", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/ListDomainHealth": { - "get": { - "description": "ListDomainHealth", - "summary": "ListDomainHealth", - "tags": ["GET"], + }, + "x-cipp-role": "CIPP.Core.Read", "parameters": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "Action", - "in": "query" - }, - { - "required": true, + "name": "FunctionType", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "Record", - "in": "query" + } }, { - "required": true, + "name": "Interval", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "ExpectedInclude", - "in": "query" + } }, { - "required": true, - "schema": { - "type": "string" - }, - "name": "Domain", - "in": "query" + "$ref": "#/components/parameters/tenantFilter" }, { - "required": true, + "name": "Time", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "Subdomains", - "in": "query" - }, + } + } + ] + } + }, + "/api/ListGDAPAccessAssignments": { + "get": { + "summary": "ListGDAPAccessAssignments", + "tags": [ + "Tenant > GDAP" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "Selector", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/ExecUniversalSearch": { - "get": { - "description": "ExecUniversalSearch", - "summary": "ExecUniversalSearch", - "tags": ["GET"], + }, + "x-cipp-role": "Tenant.Relationship.Read", "parameters": [ { - "required": true, + "name": "Id", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "name", - "in": "query" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" - } - } - }, - "description": "Successful operation" + } } - } + ] } }, - "/ListMailboxCAS": { + "/api/ListGDAPInvite": { "get": { - "description": "ListMailboxCAS", - "summary": "ListMailboxCAS", - "tags": ["GET"], - "parameters": [ + "summary": "ListGDAPInvite", + "tags": [ + "Tenant > GDAP" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/RemoveExConnectorTemplate": { - "get": { - "description": "RemoveExConnectorTemplate", - "summary": "RemoveExConnectorTemplate", - "tags": ["GET"], + }, + "x-cipp-role": "Tenant.Relationship.Read", "parameters": [ { - "required": true, + "name": "RelationshipId", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "id", - "in": "query" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" - } - } - }, - "description": "Successful operation" + } } - } + ] } }, - "/ListUserCounts": { + "/api/ListGDAPRoles": { "get": { - "description": "ListUserCounts", - "summary": "ListUserCounts", - "tags": ["GET"], - "parameters": [ + "summary": "ListGDAPRoles", + "tags": [ + "Tenant > GDAP" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } + }, + "x-cipp-role": "Tenant.Relationship.Read" } }, - "/ExecGetLocalAdminPassword": { + "/api/ListGenericTestFunction": { "get": { - "description": "ExecGetLocalAdminPassword", - "summary": "ExecGetLocalAdminPassword", - "tags": ["GET"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "guid", - "in": "query" - }, + "summary": "ListGenericTestFunction", + "tags": [ + "Uncategorized" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } + }, + "x-cipp-role": "CIPP.Core.Read" } }, - "/ListOrg": { + "/api/ListGitHubReleaseNotes": { "get": { - "description": "ListOrg", - "summary": "ListOrg", - "tags": ["GET"], - "parameters": [ + "summary": "Retrieves release notes for a GitHub repository.", + "tags": [ + "Tools > GitHub" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" - } - } - } - }, - "/ExecExcludeLicenses": { - "post": { - "description": "ExecExcludeLicenses", - "summary": "ExecExcludeLicenses", - "tags": ["POST"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "SKUName", - "in": "body" - }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "AddExclusion", - "in": "body" + } }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "RemoveExclusion", - "in": "body" + "400": { + "description": "Bad request — missing required field or invalid input" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "GUID", - "in": "body" + "401": { + "description": "Unauthorized — invalid or missing bearer token" }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Returns release metadata for the provided repository and semantic version. Hotfix\n versions (e.g. v8.5.2) map back to the base release tag (v8.5.0).\n .FUNCTIONALITY\n Entrypoint,AnyTenant\n .ROLE\n CIPP.Core.Read\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", + "x-cipp-role": "CIPP.Core.Read", + "parameters": [ { - "required": true, + "name": "Owner", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "TenantFilter", - "in": "body" + } }, { - "required": true, + "name": "Repository", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "List", - "in": "body" + } + } + ] + } + }, + "/api/ListGlobalAddressList": { + "get": { + "summary": "ListGlobalAddressList", + "tags": [ + "Email-Exchange > Reports" + ], + "security": [ + { + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" - } - } - }, - "get": { - "description": "ExecExcludeLicenses", - "summary": "ExecExcludeLicenses", - "tags": ["GET"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "SKUName", - "in": "query" + } }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "AddExclusion", - "in": "query" + "400": { + "description": "Bad request — missing required field or invalid input" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "RemoveExclusion", - "in": "query" + "401": { + "description": "Unauthorized — invalid or missing bearer token" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "GUID", - "in": "query" + "403": { + "description": "Forbidden — caller lacks the required RBAC role" }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Mailbox.Read", + "parameters": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" - }, + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListGraphBulkRequest": { + "post": { + "summary": "ListGraphBulkRequest", + "tags": [ + "CIPP > Core" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "List", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Core.Read", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "asApp": { + "type": "string" + }, + "noPaginateIds": { + "type": "string" + }, + "requests": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } } } } }, - "/ExecDeleteGDAPRelationship": { + "/api/ListGraphExplorerPresets": { "get": { - "description": "ExecDeleteGDAPRelationship", - "summary": "ExecDeleteGDAPRelationship", - "tags": ["GET"], - "parameters": [ + "summary": "ListGraphExplorerPresets", + "tags": [ + "Uncategorized" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "GDAPId", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/AddMSPApp": { - "post": { - "description": "AddMSPApp", - "summary": "AddMSPApp", - "tags": ["POST"], + }, + "x-cipp-role": "CIPP.Core.Read", "parameters": [ { - "required": true, + "name": "Endpoint", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "selectedTenants", - "in": "body" - }, + } + } + ] + } + }, + "/api/ListGraphRequest": { + "get": { + "summary": "ListGraphRequest", + "tags": [ + "CIPP > Core" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "AssignTo", - "in": "body" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/EditPolicy": { - "post": { - "description": "EditPolicy", - "summary": "EditPolicy", - "tags": ["POST"], + }, + "x-cipp-role": "CIPP.Core.Read", "parameters": [ { - "required": true, + "name": "AsApp", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "Description", - "in": "body" + } }, { - "required": true, + "name": "CountOnly", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "Displayname", - "in": "body" + } }, { - "required": true, + "name": "Endpoint", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "tenantid", - "in": "body" + } }, { - "required": true, + "name": "expand", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "Assignto", - "in": "body" + } + }, + { + "name": "graphFilter", + "in": "query", + "required": false, + "schema": { + "type": "string", + "description": "OData $filter expression passed to Graph" + } }, { - "required": true, + "name": "IgnoreErrors", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "groupid", - "in": "body" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" - } - } - }, - "description": "Successful operation" - } - } - } - }, - "/ExecDeviceAction": { - "post": { - "description": "ExecDeviceAction", - "summary": "ExecDeviceAction", - "tags": ["POST"], - "parameters": [ + } + }, { - "required": true, + "name": "ListProperties", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "input", - "in": "body" + } }, { - "required": true, + "name": "manualPagination", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "TenantFilter", - "in": "body" + } }, { - "required": true, + "name": "nextLink", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "Action", - "in": "body" + } }, { - "required": true, + "name": "NoPagination", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "GUID", - "in": "body" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" - } - } - }, - "description": "Successful operation" - } - } - }, - "get": { - "description": "ExecDeviceAction", - "summary": "ExecDeviceAction", - "tags": ["GET"], - "parameters": [ + } + }, + { + "name": "QueueId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, { - "required": true, + "name": "QueueNameOverride", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "ReverseTenantLookup", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "ReverseTenantLookupProperty", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "input", - "in": "query" + } }, { - "required": true, + "name": "SkipCache", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "TenantFilter", - "in": "query" + } }, { - "required": true, + "name": "Sort", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "Action", - "in": "query" + } }, { - "required": true, + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "Version", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "GUID", - "in": "query" + } } ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "gdapRoles": { + "$ref": "#/components/schemas/LabelValue" + }, + "id": { + "$ref": "#/components/schemas/LabelValue" + }, + "ignoreMissingRoles": { + "type": "boolean" + }, + "ooo": { + "type": "object", + "properties": { + "AutoReplyState": { + "$ref": "#/components/schemas/LabelValue" + }, + "EndTime": { + "type": "string", + "format": "date-time" + }, + "ExternalMessage": { + "type": "string" + }, + "InternalMessage": { + "type": "string" + }, + "StartTime": { + "type": "string", + "format": "date-time" + } + } + }, + "recipientLimits": { + "type": "object", + "properties": { + "MaxRecipients": { + "type": "number" + } + } + }, + "remapRoles": { + "type": "string" + }, + "standardsExcludeAllTenants": { + "type": "boolean" + } } } - }, - "description": "Successful operation" + } } } } }, - "/AddWinGetApp": { - "post": { - "description": "AddWinGetApp", - "summary": "AddWinGetApp", - "tags": ["POST"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "InstallationIntent", - "in": "body" - }, + "/api/ListGroupSenderAuthentication": { + "get": { + "summary": "ListGroupSenderAuthentication", + "tags": [ + "Identity > Administration > Groups" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "AssignTo", - "in": "body" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/RemovePolicy": { - "get": { - "description": "RemovePolicy", - "summary": "RemovePolicy", - "tags": ["GET"], + }, + "x-cipp-role": "Exchange.Groups.Read", "parameters": [ { - "required": true, + "name": "groupid", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "TenantFilter", - "in": "query" + } }, { - "required": true, - "schema": { - "type": "string" - }, - "name": "ID", - "in": "query" + "$ref": "#/components/parameters/tenantFilter" }, { - "required": true, - "schema": { - "type": "string" - }, - "name": "URLName", - "in": "query" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" - } - } - }, - "description": "Successful operation" - } - } - } - }, - "/AddExConnector": { - "post": { - "description": "AddExConnector", - "summary": "AddExConnector", - "tags": ["POST"], - "parameters": [ - { - "required": true, + "name": "Type", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "PowerShellCommand", - "in": "body" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" - } - } - }, - "description": "Successful operation" + } } - } + ] } }, - "/ListTeamsVoice": { + "/api/ListGroupTemplates": { "get": { - "description": "ListTeamsVoice", - "summary": "ListTeamsVoice", - "tags": ["GET"], - "parameters": [ + "summary": "ListGroupTemplates", + "tags": [ + "Identity > Administration > Groups" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/ListTeamsActivity": { - "get": { - "description": "ListTeamsActivity", - "summary": "ListTeamsActivity", - "tags": ["GET"], + }, + "x-cipp-role": "Identity.Group.Read", "parameters": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" - }, - { - "required": true, + "name": "id", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "Type", - "in": "query" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" - } - } - }, - "description": "Successful operation" + } } - } + ] } }, - "/ListSpamFilterTemplates": { + "/api/ListGroups": { "get": { - "description": "ListSpamFilterTemplates", - "summary": "ListSpamFilterTemplates", - "tags": ["GET"], - "parameters": [ + "summary": "ListGroups", + "tags": [ + "Identity > Administration > Groups" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "ID", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/ExecCopyForSent": { - "get": { - "description": "ExecCopyForSent", - "summary": "ExecCopyForSent", - "tags": ["GET"], + }, + "x-cipp-role": "Identity.Group.Read", "parameters": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "MessageCopyForSentAsEnabled", - "in": "query" - }, - { - "required": true, + "name": "expandMembers", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "TenantFilter", - "in": "query" + } }, { - "required": true, - "schema": { - "type": "string" - }, - "name": "id", - "in": "query" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" - } - } - }, - "description": "Successful operation" - } - } - } - }, - "/ListAzureADConnectStatus": { - "get": { - "description": "ListAzureADConnectStatus", - "summary": "ListAzureADConnectStatus", - "tags": ["GET"], - "parameters": [ - { - "required": true, + "name": "groupID", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "TenantFilter", - "in": "query" + } }, { - "required": true, + "name": "groupType", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "DataToReturn", - "in": "query" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" - } - } - }, - "description": "Successful operation" - } - } - } - }, - "/ExecEnableArchive": { - "get": { - "description": "ExecEnableArchive", - "summary": "ExecEnableArchive", - "tags": ["GET"], - "parameters": [ + } + }, { - "required": true, + "name": "members", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "id", - "in": "query" + } }, { - "required": true, + "name": "owners", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "TenantFilter", - "in": "query" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListHaloClients": { + "get": { + "summary": "ListHaloClients", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } + }, + "x-cipp-role": "CIPP.Extension.Read", + "x-cipp-dynamic-options": true } }, - "/ExecGetRecoveryKey": { + "/api/ListIPWhitelist": { "get": { - "description": "ExecGetRecoveryKey", - "summary": "ExecGetRecoveryKey", - "tags": ["GET"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" - }, + "summary": "ListIPWhitelist", + "tags": [ + "Uncategorized" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "GUID", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } + }, + "x-cipp-role": "CIPP.Core.Read" } }, - "/ListSharedMailboxStatistics": { + "/api/ListInactiveAccounts": { "get": { - "description": "ListSharedMailboxStatistics", - "summary": "ListSharedMailboxStatistics", - "tags": ["GET"], - "parameters": [ + "summary": "ListInactiveAccounts", + "tags": [ + "Identity > Reports" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/ListPotentialApps": { - "post": { - "description": "ListPotentialApps", - "summary": "ListPotentialApps", - "tags": ["POST"], + }, + "x-cipp-role": "Tenant.Directory.Read", "parameters": [ { - "required": true, + "name": "InactiveDays", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "type", - "in": "body" + } }, { - "required": true, - "schema": { - "type": "string" - }, - "name": "SearchString", - "in": "body" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" - } - } - }, - "description": "Successful operation" + "$ref": "#/components/parameters/tenantFilter" } - } + ] } }, - "/ExecCPVPermissions": { + "/api/ListIntuneIntents": { "get": { - "description": "ExecCPVPermissions", - "summary": "ExecCPVPermissions", - "tags": ["GET"], - "parameters": [ + "summary": "ListIntuneIntents", + "tags": [ + "Uncategorized" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/ListSharepointQuota": { - "get": { - "description": "ListSharepointQuota", - "summary": "ListSharepointQuota", - "tags": ["GET"], + }, + "x-cipp-role": "Endpoint.MEM.Read", "parameters": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" - } - } - }, - "description": "Successful operation" + "$ref": "#/components/parameters/tenantFilter" } - } + ] } }, - "/ListDefenderTVM": { + "/api/ListIntunePolicy": { "get": { - "description": "ListDefenderTVM", - "summary": "ListDefenderTVM", - "tags": ["GET"], - "parameters": [ + "summary": "ListIntunePolicy", + "tags": [ + "Endpoint > MEM" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" - } - } - } - }, - "/ExecEditMailboxPermissions": { - "post": { - "description": "ExecEditMailboxPermissions", - "summary": "ExecEditMailboxPermissions", - "tags": ["POST"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "AddSendAs", - "in": "body" + } }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "tenantfilter", - "in": "body" + "400": { + "description": "Bad request — missing required field or invalid input" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "userID", - "in": "body" + "401": { + "description": "Unauthorized — invalid or missing bearer token" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "AddFullAccess", - "in": "body" + "403": { + "description": "Forbidden — caller lacks the required RBAC role" }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.MEM.Read", + "parameters": [ { - "required": true, + "name": "ID", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "AccessAutomap", - "in": "body" + } }, { - "required": true, - "schema": { - "type": "string" - }, - "name": "AddFullAccessNoAutoMap", - "in": "body" + "$ref": "#/components/parameters/tenantFilter" }, { - "required": true, + "name": "URLName", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "RemoveFullAccess", - "in": "body" - }, + } + } + ] + } + }, + "/api/ListIntuneReusableSettingTemplates": { + "get": { + "summary": "ListIntuneReusableSettingTemplates", + "tags": [ + "Endpoint > MEM" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "RemoveSendAs", - "in": "body" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" - } - } - } - }, - "/AddOfficeApp": { - "post": { - "description": "AddOfficeApp", - "summary": "AddOfficeApp", - "tags": ["POST"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "AcceptLicense", - "in": "body" - }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "languages", - "in": "body" - }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "SharedComputerActivation", - "in": "body" + } }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "updateChannel", - "in": "body" + "400": { + "description": "Bad request — missing required field or invalid input" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "excludedApps", - "in": "body" + "401": { + "description": "Unauthorized — invalid or missing bearer token" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "arch", - "in": "body" + "403": { + "description": "Forbidden — caller lacks the required RBAC role" }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.MEM.Read", + "parameters": [ { - "required": true, + "name": "ID", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "Assignto", - "in": "body" - }, + } + } + ] + } + }, + "/api/ListIntuneReusableSettings": { + "get": { + "summary": "ListIntuneReusableSettings", + "tags": [ + "Endpoint > MEM" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "RemoveVersions", - "in": "body" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/EditSpamFilter": { - "get": { - "description": "EditSpamFilter", - "summary": "EditSpamFilter", - "tags": ["GET"], + }, + "x-cipp-role": "Endpoint.MEM.Read", "parameters": [ { - "required": true, + "name": "ID", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "tenantfilter", - "in": "query" + } }, { - "required": true, - "schema": { - "type": "string" - }, - "name": "name", - "in": "query" - }, + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListIntuneScript": { + "get": { + "summary": "ListIntuneScript", + "tags": [ + "Endpoint > MEM" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "state", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } + }, + "x-cipp-role": "Endpoint.MEM.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] } }, - "/ListSignIns": { + "/api/ListIntuneTemplates": { "get": { - "description": "ListSignIns", - "summary": "ListSignIns", - "tags": ["GET"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" - }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "failedlogonOnly", - "in": "query" - }, + "summary": "ListIntuneTemplates", + "tags": [ + "Endpoint > MEM" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "Filter", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/ExecDnsConfig": { - "get": { - "description": "ExecDnsConfig", - "summary": "ExecDnsConfig", - "tags": ["GET"], + }, + "x-cipp-role": "Endpoint.MEM.Read", "parameters": [ { - "required": true, + "name": "id", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "Action", - "in": "query" + } }, { - "required": true, + "name": "mode", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "Resolver", - "in": "query" + } }, { - "required": true, + "name": "View", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "Domain", - "in": "query" + } + } + ] + } + }, + "/api/ListJITAdmin": { + "get": { + "summary": "ListJITAdmin", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ + { + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" - } - } - } - }, - "/ExecEmailForward": { - "post": { - "description": "ExecEmailForward", - "summary": "ExecEmailForward", - "tags": ["POST"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "disableForwarding", - "in": "body" + } }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "tenantfilter", - "in": "body" + "400": { + "description": "Bad request — missing required field or invalid input" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "ForwardExternal", - "in": "body" + "401": { + "description": "Unauthorized — invalid or missing bearer token" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "ForwardInternal", - "in": "body" + "403": { + "description": "Forbidden — caller lacks the required RBAC role" }, + "500": { + "description": "Internal server error" + } + }, + "description": "List Just-in-time admin users for a tenant or all tenants.\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", + "x-cipp-role": "Identity.Role.Read", + "parameters": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "userID", - "in": "body" - }, + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListJITAdminTemplates": { + "post": { + "summary": "ListJITAdminTemplates", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "KeepCopy", - "in": "body" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/ExecBECRemediate": { - "post": { - "description": "ExecBECRemediate", - "summary": "ExecBECRemediate", - "tags": ["POST"], + }, + "x-cipp-role": "Identity.Role.Read", "parameters": [ { - "required": true, + "name": "GUID", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "tenantfilter", - "in": "body" + } }, { - "required": true, + "name": "includeAllTenants", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "userid", - "in": "body" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" } ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "includeAllTenants": { + "type": "string" + } } } - }, - "description": "Successful operation" + } } } } }, - "/ListPartnerRelationships": { + "/api/ListKnownIPDb": { "get": { - "description": "ListPartnerRelationships", - "summary": "ListPartnerRelationships", - "tags": ["GET"], - "parameters": [ + "summary": "ListKnownIPDb", + "tags": [ + "Uncategorized" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/ListAppsRepository": { - "post": { - "description": "ListAppsRepository", - "summary": "ListAppsRepository", - "tags": ["POST"], + }, + "x-cipp-role": "CIPP.Core.Read", "parameters": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "Repository", - "in": "body" - }, + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListLicenses": { + "get": { + "summary": "ListLicenses", + "tags": [ + "Tenant > Reports" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "Search", - "in": "body" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } + }, + "x-cipp-role": "Tenant.Directory.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] } }, - "/ExecClrImmId": { + "/api/ListLogs": { "get": { - "description": "ExecClrImmId", - "summary": "ExecClrImmId", - "tags": ["GET"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" - }, + "summary": "ListLogs", + "tags": [ + "Uncategorized" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "ID", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/AddGroupTemplate": { - "post": { - "description": "AddGroupTemplate", - "summary": "AddGroupTemplate", - "tags": ["POST"], + }, + "x-cipp-role": "CIPP.Core.Read", "parameters": [ { - "required": true, + "name": "API", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "membershipRule", - "in": "body" + } }, { - "required": true, + "name": "DateFilter", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "displayname", - "in": "body" + } }, { - "required": true, + "name": "EndDate", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "allowExternal", - "in": "body" + } }, { - "required": true, + "name": "Filter", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "groupType", - "in": "body" + } }, { - "required": true, + "name": "ListLogs", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "description", - "in": "body" + } }, { - "required": true, + "name": "logentryid", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "username", - "in": "body" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" - } - } - }, - "description": "Successful operation" - } - } - } - }, - "/Standards_IntuneTemplate": { - "post": { - "description": "Standards_IntuneTemplate", - "summary": "Standards_IntuneTemplate", - "tags": ["POST"], - "parameters": [ + } + }, { - "required": true, + "name": "ScheduledTaskId", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "Displayname", - "in": "body" + } }, { - "required": true, + "name": "Severity", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "RawJSON", - "in": "body" + } }, { - "required": true, + "name": "StandardTemplateId", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "Description", - "in": "body" + } }, { - "required": true, + "name": "StartDate", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "Type", - "in": "body" + } }, { - "required": true, + "name": "Tenant", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "Assignto", - "in": "body" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" - } - } - }, - "description": "Successful operation" + } + }, + { + "name": "User", + "in": "query", + "required": false, + "schema": { + "type": "string" + } } - } + ] } }, - "/ListIntuneTemplates": { + "/api/ListMFAUsers": { "get": { - "description": "ListIntuneTemplates", - "summary": "ListIntuneTemplates", - "tags": ["GET"], - "parameters": [ + "summary": "ListMFAUsers", + "tags": [ + "Identity > Reports" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "ID", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/ExecResetPass": { - "get": { - "description": "ExecResetPass", - "summary": "ExecResetPass", - "tags": ["GET"], + }, + "x-cipp-role": "Identity.User.Read", "parameters": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" - }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "ID", - "in": "query" + "$ref": "#/components/parameters/tenantFilter" }, { - "required": true, + "name": "UseReportDB", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "MustChange", - "in": "query" - }, + } + } + ] + } + }, + "/api/ListMailQuarantine": { + "get": { + "summary": "ListMailQuarantine", + "tags": [ + "Email-Exchange > Spamfilter" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "displayName", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } + }, + "x-cipp-role": "Exchange.SpamFilter.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] } }, - "/ExecAlertsList": { + "/api/ListMailQuarantineMessage": { "get": { - "description": "ExecAlertsList", - "summary": "ExecAlertsList", - "tags": ["GET"], - "parameters": [ + "summary": "ListMailQuarantineMessage", + "tags": [ + "Email-Exchange > Spamfilter" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/ExecQuarantineManagement": { - "get": { - "description": "ExecQuarantineManagement", - "summary": "ExecQuarantineManagement", - "tags": ["GET"], + }, + "x-cipp-role": "Exchange.SpamFilter.Read", "parameters": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" - }, - { - "required": true, + "name": "Identity", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "ID", - "in": "query" + } }, { - "required": true, - "schema": { - "type": "string" - }, - "name": "type", - "in": "query" - }, + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListMailboxCAS": { + "get": { + "summary": "ListMailboxCAS", + "tags": [ + "Email-Exchange > Reports" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "AllowSender", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } + }, + "x-cipp-role": "Exchange.Mailbox.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] } }, - "/ExecRestoreDeleted": { + "/api/ListMailboxForwarding": { "get": { - "description": "ExecRestoreDeleted", - "summary": "ExecRestoreDeleted", - "tags": ["GET"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" - }, + "summary": "ListMailboxForwarding", + "tags": [ + "Email-Exchange > Reports" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "ID", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/ListUserGroups": { - "get": { - "description": "ListUserGroups", - "summary": "ListUserGroups", - "tags": ["GET"], + }, + "x-cipp-role": "Exchange.Mailbox.Read", "parameters": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" + "$ref": "#/components/parameters/tenantFilter" }, { - "required": true, - "schema": { - "type": "string" - }, - "name": "UserID", - "in": "query" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" - } - } - }, - "description": "Successful operation" + "name": "UseReportDB", + "in": "query", + "required": false, + "schema": { + "type": "string" + } } - } + ] } }, - "/RemoveStandard": { + "/api/ListMailboxMobileDevices": { "get": { - "description": "RemoveStandard", - "summary": "RemoveStandard", - "tags": ["GET"], - "parameters": [ + "summary": "ListMailboxMobileDevices", + "tags": [ + "Email-Exchange > Administration" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "id", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/ListUserConditionalAccessPolicies": { - "get": { - "description": "ListUserConditionalAccessPolicies", - "summary": "ListUserConditionalAccessPolicies", - "tags": ["GET"], + }, + "x-cipp-role": "Exchange.Mailbox.Read", "parameters": [ { - "required": true, + "name": "Mailbox", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "TenantFilter", - "in": "query" + } }, { - "required": true, - "schema": { - "type": "string" - }, - "name": "UserID", - "in": "query" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" - } - } - }, - "description": "Successful operation" + "$ref": "#/components/parameters/tenantFilter" } - } + ] } }, - "/ListCAtemplates": { + "/api/ListMailboxRestores": { "get": { - "description": "ListCAtemplates", - "summary": "ListCAtemplates", - "tags": ["GET"], - "parameters": [ + "summary": "ListMailboxRestores", + "tags": [ + "Email-Exchange > Tools" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "id", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/ListContacts": { - "get": { - "description": "ListContacts", - "summary": "ListContacts", - "tags": ["GET"], + }, + "x-cipp-role": "Exchange.Mailbox.Read", "parameters": [ { - "required": true, + "name": "Identity", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "TenantFilter", - "in": "query" + } }, { - "required": true, + "name": "IncludeReport", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "ContactID", - "in": "query" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" - } - } - }, - "description": "Successful operation" + } + }, + { + "name": "Statistics", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" } - } + ] } }, - "/ListContactTemplates": { + "/api/ListMailboxRules": { "get": { - "description": "List Contact Templates", - "summary": "List Contact Templates", - "tags": ["GET"], + "summary": "ListMailboxRules", + "tags": [ + "Email-Exchange > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "type": "object", - "properties": {} - } + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/RemoveContactTemplates": { - "get": { - "description": "Remove Contact Template", - "summary": "Remove Contact Template", - "tags": ["GET"], + }, + "x-cipp-role": "Exchange.Mailbox.Read", "parameters": [ { - "required": true, + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "UseReportDB", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "id", - "in": "query" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" - } - } - }, - "description": "Successful operation" + } } - } + ] } }, - "/AddContactTemplates": { - "post": { - "description": "Add Contact Template", - "summary": "Add Contact Template", - "tags": ["POST"], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": {} - } - } + "/api/ListMailboxes": { + "get": { + "summary": "ListMailboxes", + "tags": [ + "Email-Exchange > Administration" + ], + "security": [ + { + "bearerAuth": [] } - }, + ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/ListContactPermissions": { - "get": { - "description": "ListContactPermissions - Retrieves contact folder permissions for a specified user", - "summary": "ListContactPermissions", - "tags": ["GET"], + }, + "x-cipp-role": "Exchange.Mailbox.Read", "parameters": [ { - "required": true, + "name": "PSObject", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "UserID", - "in": "query", - "description": "The user ID to retrieve contact permissions for" + } }, { - "required": true, + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "UseReportDB", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "tenantFilter", - "in": "query", - "description": "The tenant filter to specify which tenant" + } + } + ] + } + }, + "/api/ListMalwareFilters": { + "get": { + "summary": "ListMalwareFilters", + "tags": [ + "Email-Exchange > Reports" + ], + "security": [ + { + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "Identity": { - "type": "string", - "description": "The identity of the contact folder" - }, - "User": { - "type": "string", - "description": "The user who has permissions" - }, - "AccessRights": { - "type": "array", - "items": { - "type": "string" - }, - "description": "The access rights granted to the user" - }, - "FolderName": { - "type": "string", - "description": "The name of the contact folder" - }, - "MailboxInfo": { - "type": "object", - "description": "Information about the mailbox" - } - } - } + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successfully retrieved contact permissions" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" }, "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.SpamFilter.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListMessageTrace": { + "post": { + "summary": "ListMessageTrace", + "tags": [ + "Email-Exchange > Tools" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "error": { - "type": "string", - "description": "Error message" - } - } + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Forbidden - insufficient permissions" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/ExecModifyContactPerms": { - "post": { - "description": "ExecModifyContactPerms - Modifies contact folder permissions for a specified user", - "summary": "ExecModifyContactPerms", - "tags": ["POST"], + }, + "x-cipp-role": "Exchange.TransportRule.Read", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", - "required": ["userID", "tenantFilter", "permissions"], "properties": { - "userID": { + "dateFilter": { + "type": "string", + "enum": [ + "relative", + "startEnd" + ] + }, + "days": { + "type": "number" + }, + "endDate": { "type": "string", - "description": "The user ID whose contact permissions will be modified" + "format": "date-time" + }, + "fromIP": { + "type": "string" + }, + "ID": { + "type": "string" + }, + "recipient": { + "type": "array", + "items": { + "type": "string" + } + }, + "sender": { + "type": "array", + "items": { + "type": "string" + } }, - "tenantFilter": { + "startDate": { "type": "string", - "description": "The tenant filter to specify which tenant" + "format": "date-time" }, - "permissions": { + "status": { "type": "array", - "description": "Array of permission objects to apply", "items": { - "type": "object", - "properties": { - "PermissionLevel": { - "type": "object", - "properties": { - "value": { - "type": "string", - "description": "Permission level (e.g., Owner, PublishingEditor, Editor, PublishingAuthor, Author, NonEditingAuthor, Reviewer, Contributor, AvailabilityOnly, LimitedDetails)" - } - } - }, - "Modification": { - "type": "string", - "description": "Type of modification (Add, Remove)", - "enum": ["Add", "Remove"] - }, - "UserID": { - "type": "array", - "description": "Array of target users to grant/remove permissions", - "items": { - "type": "object", - "properties": { - "value": { - "type": "string", - "description": "User ID or email address" - } - } - } - }, - "CanViewPrivateItems": { - "type": "boolean", - "description": "Whether the user can view private items", - "default": false - }, - "FolderName": { - "type": "string", - "description": "Name of the contact folder", - "default": "Contact" - }, - "SendNotificationToUser": { - "type": "boolean", - "description": "Whether to send notification to the user", - "default": false - } - }, - "required": ["PermissionLevel", "Modification", "UserID"] + "type": "string" } + }, + "tenantFilter": { + "type": "string" + }, + "toIP": { + "type": "string" + }, + "traceDetail": { + "type": "string" + }, + "MessageId": { + "type": "string" } - } + }, + "required": [ + "tenantFilter" + ] } } } - }, + } + } + }, + "/api/ListNamedLocations": { + "get": { + "summary": "ListNamedLocations", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] + } + ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "Results": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Array of result messages for each permission operation" - } - } + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successfully processed contact permission modifications" + } }, "400": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "Results": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Error messages" - } - } - } - } - }, - "description": "Bad Request - Missing required parameters" + "description": "Bad request — missing required field or invalid input" }, - "404": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "Results": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Error messages" - } - } - } - } - }, - "description": "Not Found - User ID not found" + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" }, "500": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "Results": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Error messages" - } - } - } - } - }, - "description": "Internal Server Error - Operation failed" + "description": "Internal server error" } - } + }, + "x-cipp-role": "Tenant.ConditionalAccess.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] } }, - "/ListMailboxRules": { - "get": { - "description": "ListMailboxRules", - "summary": "ListMailboxRules", - "tags": ["GET"], - "parameters": [ + "/api/ListNewUserDefaults": { + "post": { + "summary": "ListNewUserDefaults", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/ListSites": { - "get": { - "description": "ListSites", - "summary": "ListSites", - "tags": ["GET"], + }, + "x-cipp-role": "Identity.User.Read", "parameters": [ { - "required": true, + "name": "ID", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "Type", - "in": "query" + } }, { - "required": true, + "name": "includeAllTenants", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "TenantFilter", - "in": "query" + } }, { - "required": true, - "schema": { - "type": "string" - }, - "name": "UserUPN", - "in": "query" + "$ref": "#/components/parameters/tenantFilter" } ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "includeAllTenants": { + "type": "string" + } } } - }, - "description": "Successful operation" + } } } } }, - "/ExecSetOoO": { - "post": { - "description": "ExecSetOoO", - "summary": "ExecSetOoO", - "tags": ["POST"], - "parameters": [ + "/api/ListNotificationConfig": { + "get": { + "summary": "ListNotificationConfig", + "tags": [ + "Uncategorized" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "tenantfilter", - "in": "body" + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "user", - "in": "body" + "400": { + "description": "Bad request — missing required field or invalid input" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "input", - "in": "body" + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.AppSettings.Read", + "x-cipp-dynamic-options": true + } + }, + "/api/ListOAuthApps": { + "get": { + "summary": "ListOAuthApps", + "tags": [ + "Tenant > Reports" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "Disable", - "in": "body" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/AddExConnectorTemplate": { - "post": { - "description": "AddExConnectorTemplate", - "summary": "AddExConnectorTemplate", - "tags": ["POST"], + }, + "x-cipp-role": "Tenant.Application.Read", "parameters": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "name", - "in": "body" - }, + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListOoO": { + "get": { + "summary": "ListOoO", + "tags": [ + "Email-Exchange > Administration" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "cippconnectortype", - "in": "body" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/ExecOffboardUser": { - "post": { - "description": "ExecOffboardUser", - "summary": "ExecOffboardUser", - "tags": ["POST"], + }, + "x-cipp-role": "Exchange.Mailbox.Read", "parameters": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "KeepCopy", - "in": "body" + "$ref": "#/components/parameters/tenantFilter" }, { - "required": true, + "name": "userid", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "OnedriveAccess", - "in": "body" - }, + } + } + ] + } + }, + "/api/ListOrg": { + "get": { + "summary": "ListOrg", + "tags": [ + "Uncategorized" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "tenantfilter", - "in": "body" + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "OOO", - "in": "body" + "400": { + "description": "Bad request — missing required field or invalid input" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "AccessNoAutomap", - "in": "body" + "401": { + "description": "Unauthorized — invalid or missing bearer token" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "forward", - "in": "body" + "403": { + "description": "Forbidden — caller lacks the required RBAC role" }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Core.Read", + "parameters": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "user", - "in": "body" + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListPartnerRelationships": { + "get": { + "summary": "ListPartnerRelationships", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } + }, + "x-cipp-role": "Tenant.Relationship.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] } }, - "/ListTenantDetails": { + "/api/ListPendingWebhooks": { "get": { - "description": "ListTenantDetails", - "summary": "ListTenantDetails", - "tags": ["GET"], - "parameters": [ + "summary": "ListPendingWebhooks", + "tags": [ + "Uncategorized" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } + }, + "x-cipp-role": "CIPP.Alert.Read" } }, - "/ExecConverttoSharedMailbox": { + "/api/ListPerUserMFA": { "get": { - "description": "ExecConverttoSharedMailbox", - "summary": "ExecConverttoSharedMailbox", - "tags": ["GET"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" - }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "ConvertToUser", - "in": "query" - }, + "summary": "ListPerUserMFA", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "id", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/ExecGraphRequest": { - "get": { - "description": "ExecGraphRequest", - "summary": "ExecGraphRequest", - "tags": ["GET"], + }, + "x-cipp-role": "Identity.User.Read", "parameters": [ { - "required": true, + "name": "allUsers", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "TenantFilter", - "in": "query" + } }, { - "required": true, - "schema": { - "type": "string" - }, - "name": "Endpoint", - "in": "query" + "$ref": "#/components/parameters/tenantFilter" }, { - "required": true, + "name": "userId", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "DisablePagination", - "in": "query" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" - } - } - }, - "description": "Successful operation" + } } - } + ] } }, - "/ListDefenderState": { - "get": { - "description": "ListDefenderState", - "summary": "ListDefenderState", - "tags": ["GET"], - "parameters": [ + "/api/ListPotentialApps": { + "post": { + "summary": "ListPotentialApps", + "tags": [ + "Uncategorized" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.Application.Read", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "AcceptLicense": { + "type": "boolean" + }, + "applicationName": { + "type": "string" + }, + "appType": { + "$ref": "#/components/schemas/LabelValue" + }, + "arch": { + "type": "boolean" + }, + "AssignTo": { + "type": "string", + "enum": [ + "On", + "allLicensedUsers", + "AllDevices", + "AllDevicesAndUsers", + "customGroup" + ] + }, + "customArguments": { + "type": "string" + }, + "customGroup": { + "type": "string" + }, + "customRepo": { + "type": "string" + }, + "customXml": { + "type": "string" + }, + "description": { + "type": "string" + }, + "detectionFile": { + "type": "string" + }, + "detectionPath": { + "type": "string" + }, + "DisableRestart": { + "type": "boolean" + }, + "displayName": { + "type": "string" + }, + "enforceSignatureCheck": { + "type": "boolean" + }, + "excludedApps": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LabelValue" + } + }, + "InstallAsSystem": { + "type": "boolean" + }, + "InstallationIntent": { + "type": "boolean" + }, + "installScript": { + "type": "string" + }, + "languages": { + "$ref": "#/components/schemas/LabelValue" + }, + "packagename": { + "type": "string" + }, + "packageSearch": { + "$ref": "#/components/schemas/LabelValue" + }, + "params": { + "type": "object", + "properties": { + "AccountKey": { + "type": "string" + }, + "dattoUrl": { + "type": "string" + }, + "Server": { + "type": "string" + } + } + }, + "publisher": { + "type": "string" + }, + "RemoveVersions": { + "type": "boolean" + }, + "rmmname": { + "$ref": "#/components/schemas/LabelValue" + }, + "runAs32Bit": { + "type": "boolean" + }, + "searchQuery": { + "type": "string" + }, + "SearchString": { + "type": "string" + }, + "SharedComputerActivation": { + "type": "boolean" + }, + "type": { + "type": "string" + }, + "uninstallScript": { + "type": "string" + }, + "updateChannel": { + "$ref": "#/components/schemas/LabelValue" + }, + "useCustomXml": { + "type": "boolean" + } + } + } + } } } } }, - "/ListMailQuarantine": { - "get": { - "description": "ListMailQuarantine", - "summary": "ListMailQuarantine", - "tags": ["GET"], - "parameters": [ + "/api/ListQuarantinePolicy": { + "post": { + "summary": "ListQuarantinePolicy", + "tags": [ + "Email-Exchange > Spamfilter" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "tenantfilter", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/ListIntunePolicy": { - "get": { - "description": "ListIntunePolicy", - "summary": "ListIntunePolicy", - "tags": ["GET"], + }, + "x-cipp-role": "Exchange.SpamFilter.Read", "parameters": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" - }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "ID", - "in": "query" + "$ref": "#/components/parameters/tenantFilter" }, { - "required": true, + "name": "Type", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "URLName", - "in": "query" + } } ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" - } + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "TenantFilter": { + "type": "string" + } + }, + "required": [ + "TenantFilter" + ] } - }, - "description": "Successful operation" + } } } } }, - "/ExecRevokeSessions": { + "/api/ListRestrictedUsers": { "get": { - "description": "ExecRevokeSessions", - "summary": "ExecRevokeSessions", - "tags": ["GET"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" - }, + "summary": "ListRestrictedUsers", + "tags": [ + "Email-Exchange > Administration" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "id", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } + }, + "description": "Lists users from the restricted senders list in Exchange Online.\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)\n # Interact with query parameters or the body of the request.\n $TenantFilter = $Request.Query.tenantFilter", + "x-cipp-role": "Exchange.Mailbox.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] } }, - "/ListmailboxPermissions": { + "/api/ListRoles": { "get": { - "description": "ListmailboxPermissions", - "summary": "ListmailboxPermissions", - "tags": ["GET"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" - }, + "summary": "ListRoles", + "tags": [ + "Uncategorized" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "UserID", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } + }, + "x-cipp-role": "Identity.Role.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] } }, - "/ExecExtensionsConfig": { - "post": { - "description": "ExecExtensionsConfig", - "summary": "ExecExtensionsConfig", - "tags": ["POST"], - "parameters": [ + "/api/ListRoomLists": { + "get": { + "summary": "ListRoomLists", + "tags": [ + "Email-Exchange > Resources" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "CIPPAPI", - "in": "body" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/ListDevices": { - "get": { - "description": "ListDevices", - "summary": "ListDevices", - "tags": ["GET"], + }, + "x-cipp-role": "Exchange.Room.Read", "parameters": [ { - "required": true, + "name": "groupID", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "TenantFilter", - "in": "query" + } + }, + { + "name": "members", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "owners", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListRooms": { + "get": { + "summary": "ListRooms", + "tags": [ + "Email-Exchange > Resources" + ], + "security": [ + { + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/RemoveCATemplate": { - "get": { - "description": "RemoveCATemplate", - "summary": "RemoveCATemplate", - "tags": ["GET"], + }, + "x-cipp-role": "Exchange.Room.Read", "parameters": [ { - "required": true, + "name": "roomId", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "id", - "in": "query" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListSafeAttachmentsFilters": { + "get": { + "summary": "ListSafeAttachmentsFilters", + "tags": [ + "Email-Exchange > Reports" + ], + "security": [ + { + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } + }, + "x-cipp-role": "Exchange.SpamFilter.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] } }, - "/RemoveQueuedApp": { + "/api/ListSafeLinksPolicy": { "get": { - "description": "RemoveQueuedApp", - "summary": "RemoveQueuedApp", - "tags": ["GET"], - "parameters": [ + "summary": "ListSafeLinksPolicy", + "tags": [ + "Security > Safe-Links-Policy" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "id", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } + }, + "description": "This function is used to list the Safe Links policies in the tenant, including unmatched rules and policies.\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)\n $APIName = $Request.Params.CIPPEndpoint\n $Headers = $Request.Headers", + "x-cipp-role": "Security.SafeLinksPolicy.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] } }, - "/DomainAnalyser_List": { - "get": { - "description": "DomainAnalyser_List", - "summary": "DomainAnalyser_List", - "tags": ["GET"], - "parameters": [ + "/api/ListSafeLinksPolicyDetails": { + "post": { + "summary": "ListSafeLinksPolicyDetails", + "tags": [ + "Security > Safe-Links-Policy" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "tenantFilter", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" - } - } - } - }, - "/AddNamedLocation": { - "post": { - "description": "AddNamedLocation", - "summary": "AddNamedLocation", - "tags": ["POST"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "Ips", - "in": "body" + } }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "includeUnknownCountriesAndRegions", - "in": "body" + "400": { + "description": "Bad request — missing required field or invalid input" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "Countries", - "in": "body" + "401": { + "description": "Unauthorized — invalid or missing bearer token" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "Trusted", - "in": "body" + "403": { + "description": "Forbidden — caller lacks the required RBAC role" }, + "500": { + "description": "Internal server error" + } + }, + "description": "This function retrieves details for a specific Safe Links policy and rule.\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", + "x-cipp-role": "Exchange.SpamFilter.Read", + "parameters": [ { - "required": true, + "name": "PolicyName", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "policyName", - "in": "body" + } }, { - "required": true, + "name": "RuleName", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "Type", - "in": "body" + } }, { - "required": true, - "schema": { - "type": "string" - }, - "name": "selectedTenants", - "in": "body" + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "PolicyName": { + "type": "string" + }, + "RuleName": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ListSafeLinksPolicyTemplateDetails": { + "post": { + "summary": "ListSafeLinksPolicyTemplateDetails", + "tags": [ + "Security > Safe-Links-Policy" + ], + "security": [ + { + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "This function retrieves details for a specific Safe Links policy template.\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", + "x-cipp-role": "Exchange.SafeLinks.Read", + "parameters": [ + { + "name": "ID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ID": { + "type": "string" + } } } - }, - "description": "Successful operation" + } } } } }, - "/ListTransportRulesTemplates": { + "/api/ListSafeLinksPolicyTemplates": { "get": { - "description": "ListTransportRulesTemplates", - "summary": "ListTransportRulesTemplates", - "tags": ["GET"], - "parameters": [ + "summary": "ListSafeLinksPolicyTemplates", + "tags": [ + "Security > Safe-Links-Policy" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "ID", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" - } - } - } - }, - "/ExecEditCalendarPermissions": { - "get": { - "description": "ExecEditCalendarPermissions", - "summary": "ExecEditCalendarPermissions", - "tags": ["GET"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "permissions", - "in": "query" + } }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "tenantfilter", - "in": "query" + "400": { + "description": "Bad request — missing required field or invalid input" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "UserToGetPermissions", - "in": "query" + "401": { + "description": "Unauthorized — invalid or missing bearer token" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "UserID", - "in": "query" + "403": { + "description": "Forbidden — caller lacks the required RBAC role" }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.SafeLinks.Read", + "parameters": [ { - "required": true, + "name": "id", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "removeaccess", - "in": "query" - }, + } + } + ] + } + }, + "/api/ListScheduledItemDetails": { + "post": { + "summary": "ListScheduledItemDetails", + "tags": [ + "CIPP > Scheduler" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "folderName", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" - } - } - } - }, - "/ExecDisableUser": { - "get": { - "description": "ExecDisableUser", - "summary": "ExecDisableUser", - "tags": ["GET"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" + } }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "ID", - "in": "query" + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Scheduler.Read", + "parameters": [ { - "required": true, + "name": "RowKey", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "Enable", - "in": "query" + } } ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "RowKey": { + "type": "string" + } } } - }, - "description": "Successful operation" + } } } } }, - "/AddTransportRule": { + "/api/ListScheduledItems": { "post": { - "description": "AddTransportRule", - "summary": "AddTransportRule", - "tags": ["POST"], - "parameters": [ + "summary": "ListScheduledItems", + "tags": [ + "CIPP > Scheduler" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "PowerShellCommand", - "in": "body" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/ListTenants": { - "get": { - "description": "ListTenants", - "summary": "ListTenants", - "tags": ["GET"], + }, + "x-cipp-role": "CIPP.Scheduler.Read", "parameters": [ { - "required": true, + "name": "Id", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "TenantFilter", - "in": "query" + } }, { - "required": true, + "name": "Name", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "ClearCache", - "in": "query" + } }, { - "required": true, + "name": "SearchTitle", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "TenantsOnly", - "in": "query" + } }, { - "required": true, - "schema": { - "type": "string" - }, - "name": "AllTenantSelector", - "in": "query" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" - } - } - }, - "description": "Successful operation" - } - } - } - }, - "/ExecResetMFA": { - "get": { - "description": "ExecResetMFA", - "summary": "ExecResetMFA", - "tags": ["GET"], - "parameters": [ - { - "required": true, + "name": "ShowHidden", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "TenantFilter", - "in": "query" + } }, { - "required": true, - "schema": { - "type": "string" - }, - "name": "ID", - "in": "query" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" - } - } - }, - "description": "Successful operation" - } - } - } - }, - "/ListIntuneIntents": { - "get": { - "description": "ListIntuneIntents", - "summary": "ListIntuneIntents", - "tags": ["GET"], - "parameters": [ + "$ref": "#/components/parameters/tenantFilter" + }, { - "required": true, + "name": "Type", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "TenantFilter", - "in": "query" + } } ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" - } + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Id": { + "type": "string" + }, + "Name": { + "type": "string" + }, + "SearchTitle": { + "type": "string" + }, + "ShowHidden": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "Type": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] } - }, - "description": "Successful operation" + } } } } }, - "/ListStandards": { + "/api/ListServiceHealth": { "get": { - "description": "ListStandards", - "summary": "ListStandards", - "tags": ["GET"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" - } + "summary": "ListServiceHealth", + "tags": [ + "Tenant > Reports" ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" - } - } - }, - "description": "Successful operation" - } - } - } - }, - "/ListDeletedItems": { - "get": { - "description": "ListDeletedItems", - "summary": "ListDeletedItems", - "tags": ["GET"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" + "security": [ + { + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/ExecGroupsHideFromGAL": { - "get": { - "description": "ExecGroupsHideFromGAL", - "summary": "ExecGroupsHideFromGAL", - "tags": ["GET"], + }, + "x-cipp-role": "Tenant.Administration.Read", "parameters": [ { - "required": true, + "name": "defaultDomainName", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "TenantFilter", - "in": "query" + } }, { - "required": true, + "name": "displayName", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "groupType", - "in": "query" + } }, { - "required": true, - "schema": { - "type": "string" - }, - "name": "id", - "in": "query" - }, + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListSharedMailboxAccountEnabled": { + "get": { + "summary": "ListSharedMailboxAccountEnabled", + "tags": [ + "Email-Exchange > Reports" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "HidefromGAL", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } + }, + "x-cipp-role": "Exchange.Mailbox.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] } }, - "/ExecSendPush": { + "/api/ListSharedMailboxStatistics": { "get": { - "description": "ExecSendPush", - "summary": "ExecSendPush", - "tags": ["GET"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" - }, + "summary": "ListSharedMailboxStatistics", + "tags": [ + "Email-Exchange > Administration" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "UserEmail", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/ExecExtensionMapping": { - "post": { - "description": "ExecExtensionMapping", - "summary": "ExecExtensionMapping", - "tags": ["POST"], + }, + "x-cipp-role": "Exchange.Mailbox.Read", "parameters": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "mappings", - "in": "body" - }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "List", - "in": "body" - }, + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListSharepointAdminUrl": { + "get": { + "summary": "ListSharepointAdminUrl", + "tags": [ + "Teams-Sharepoint" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "AddMapping", - "in": "body" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - }, - "get": { - "description": "ExecExtensionMapping", - "summary": "ExecExtensionMapping", - "tags": ["GET"], + }, + "x-cipp-role": "CIPP.Core.Read", "parameters": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "mappings", - "in": "query" - }, - { - "required": true, + "name": "ReturnUrl", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "List", - "in": "query" + } }, { - "required": true, - "schema": { - "type": "string" - }, - "name": "AddMapping", - "in": "query" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" - } - } - }, - "description": "Successful operation" + "$ref": "#/components/parameters/tenantFilter" } - } + ] } }, - "/ListAppStatus": { + "/api/ListSharepointQuota": { "get": { - "description": "ListAppStatus", - "summary": "ListAppStatus", - "tags": ["GET"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" - }, + "summary": "ListSharepointQuota", + "tags": [ + "Teams-Sharepoint" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "AppFilter", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } + }, + "x-cipp-role": "Sharepoint.Admin.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] } }, - "/GetVersion": { + "/api/ListSharepointSettings": { "get": { - "description": "GetVersion", - "summary": "GetVersion", - "tags": ["GET"], - "parameters": [ + "summary": "ListSharepointSettings", + "tags": [ + "Teams-Sharepoint" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "localversion", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" - } - } - } - }, - "/ExecMailboxMobileDevices": { - "get": { - "description": "ExecMailboxMobileDevices", - "summary": "ExecMailboxMobileDevices", - "tags": ["GET"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "tenantfilter", - "in": "query" + } }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "Userid", - "in": "query" + "400": { + "description": "Bad request — missing required field or invalid input" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "Quarantine", - "in": "query" + "401": { + "description": "Unauthorized — invalid or missing bearer token" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "Delete", - "in": "query" + "403": { + "description": "Forbidden — caller lacks the required RBAC role" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "deviceid", - "in": "query" + "500": { + "description": "Internal server error" } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" - } - } - }, - "description": "Successful operation" + }, + "x-cipp-role": "Sharepoint.Admin.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" } - } + ] } }, - "/RemoveApp": { + "/api/ListSignIns": { "get": { - "description": "RemoveApp", - "summary": "RemoveApp", - "tags": ["GET"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" - }, + "summary": "ListSignIns", + "tags": [ + "Identity > Reports" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "ID", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/ExecPasswordConfig": { - "post": { - "description": "ExecPasswordConfig", - "summary": "ExecPasswordConfig", - "tags": ["POST"], + }, + "x-cipp-role": "Identity.AuditLog.Read", "parameters": [ { - "required": true, + "name": "Days", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "failedLogonsOnly", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "List", - "in": "body" + } }, { - "required": true, + "name": "FailureThreshold", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "passwordType", - "in": "body" + } + }, + { + "name": "Filter", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListSiteMembers": { + "get": { + "summary": "ListSiteMembers", + "tags": [ + "Teams-Sharepoint" + ], + "security": [ + { + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - }, - "get": { - "description": "ExecPasswordConfig", - "summary": "ExecPasswordConfig", - "tags": ["GET"], + }, + "x-cipp-role": "Sharepoint.Site.Read", "parameters": [ { - "required": true, + "name": "SiteId", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "List", - "in": "query" + } }, { - "required": true, - "schema": { - "type": "string" - }, - "name": "passwordType", - "in": "query" + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListSites": { + "get": { + "summary": "ListSites", + "tags": [ + "Teams-Sharepoint" + ], + "security": [ + { + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/ExecHideFromGAL": { - "get": { - "description": "ExecHideFromGAL", - "summary": "ExecHideFromGAL", - "tags": ["GET"], + }, + "x-cipp-role": "Sharepoint.Site.Read", "parameters": [ { - "required": true, + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "Type", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "HideFromGal", - "in": "query" + } }, { - "required": true, + "name": "URLOnly", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "tenantfilter", - "in": "query" + } }, { - "required": true, + "name": "UserUPN", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "ID", - "in": "query" + } + } + ] + } + }, + "/api/ListSpamFilterTemplates": { + "get": { + "summary": "ListSpamFilterTemplates", + "tags": [ + "Email-Exchange > Spamfilter" + ], + "security": [ + { + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/EditUser": { - "post": { - "description": "EditUser", - "summary": "EditUser", - "tags": ["POST"], + }, + "x-cipp-role": "Exchange.SpamFilter.Read", "parameters": [ { - "required": true, + "name": "id", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "CopyFrom", - "in": "body" + } + } + ] + } + }, + "/api/ListSpamfilter": { + "get": { + "summary": "ListSpamfilter", + "tags": [ + "Email-Exchange > Spamfilter" + ], + "security": [ + { + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } + }, + "x-cipp-role": "Exchange.SpamFilter.Read", + "x-cipp-dynamic-options": true, + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] } }, - "/ListOAuthApps": { + "/api/ListStandards": { "get": { - "description": "ListOAuthApps", - "summary": "ListOAuthApps", - "tags": ["GET"], - "parameters": [ + "summary": "ListStandards", + "tags": [ + "Tenant > Standards" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/ListDeviceDetails": { - "get": { - "description": "ListDeviceDetails", - "summary": "ListDeviceDetails", - "tags": ["GET"], + }, + "x-cipp-role": "Tenant.Standards.Read", "parameters": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" - }, - { - "required": true, + "name": "ShowConsolidated", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "DeviceSerial", - "in": "query" + } }, { - "required": true, - "schema": { - "type": "string" - }, - "name": "DeviceID", - "in": "query" - }, + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListStandardsCompare": { + "get": { + "summary": "ListStandardsCompare", + "tags": [ + "Tenant > Standards" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "DeviceName", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" - } - } - } - }, - "/ListLogs": { - "get": { - "description": "ListLogs", - "summary": "ListLogs", - "tags": ["GET"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "ListLogs", - "in": "query" + } }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "Severity", - "in": "query" + "400": { + "description": "Bad request — missing required field or invalid input" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "User", - "in": "query" + "401": { + "description": "Unauthorized — invalid or missing bearer token" }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.BestPracticeAnalyser.Read", + "parameters": [ { - "required": true, + "name": "templateId", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "DateFilter", - "in": "query" + } }, { - "required": true, - "schema": { - "type": "string" - }, - "name": "Filter", - "in": "query" + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListTeams": { + "get": { + "summary": "ListTeams", + "tags": [ + "Teams-Sharepoint" + ], + "security": [ + { + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Teams.Group.Read", + "parameters": [ + { + "name": "ID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "type", + "in": "query", + "required": false, + "schema": { + "type": "string" + } } - } + ] } }, - "/ListAllTenantDeviceCompliance": { + "/api/ListTeamsActivity": { "get": { - "description": "ListAllTenantDeviceCompliance", - "summary": "ListAllTenantDeviceCompliance", - "tags": ["GET"], - "parameters": [ + "summary": "ListTeamsActivity", + "tags": [ + "Teams-Sharepoint" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/ListUsers": { - "get": { - "description": "ListUsers", - "summary": "ListUsers", - "tags": ["GET"], + }, + "x-cipp-role": "Teams.Activity.Read", "parameters": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" - }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "IncludeLogonDetails", - "in": "query" + "$ref": "#/components/parameters/tenantFilter" }, { - "required": true, + "name": "Type", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "graphFilter", - "in": "query" - }, + } + } + ] + } + }, + "/api/ListTeamsLisLocation": { + "get": { + "summary": "ListTeamsLisLocation", + "tags": [ + "Teams-Sharepoint" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "UserID", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" - } - } - } - }, - "/ListMessageTrace": { - "get": { - "description": "ListMessageTrace", - "summary": "ListMessageTrace", - "tags": ["GET"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "Tracedetail", - "in": "query" + } }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" + "400": { + "description": "Bad request — missing required field or invalid input" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "sender", - "in": "query" + "401": { + "description": "Unauthorized — invalid or missing bearer token" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "days", - "in": "query" + "403": { + "description": "Forbidden — caller lacks the required RBAC role" }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Teams.Voice.Read", + "parameters": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "ID", - "in": "query" - }, + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListTeamsVoice": { + "get": { + "summary": "ListTeamsVoice", + "tags": [ + "Teams-Sharepoint" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "recipient", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } + }, + "x-cipp-role": "Teams.Voice.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] } }, - "/ListPhishPolicies": { + "/api/ListTenantAlignment": { "get": { - "description": "ListPhishPolicies", - "summary": "ListPhishPolicies", - "tags": ["GET"], - "parameters": [ + "summary": "ListTenantAlignment", + "tags": [ + "Tenant > Standards" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } + }, + "x-cipp-role": "Tenant.Standards.Read" } }, - "/RemoveSpamfilter": { + "/api/ListTenantAllowBlockList": { "get": { - "description": "RemoveSpamfilter", - "summary": "RemoveSpamfilter", - "tags": ["GET"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "tenantfilter", - "in": "query" - }, + "summary": "ListTenantAllowBlockList", + "tags": [ + "Uncategorized" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "name", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" - } - } - } - }, - "/ExecNotificationConfig": { - "post": { - "description": "ExecNotificationConfig", - "summary": "ExecNotificationConfig", - "tags": ["POST"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "Webhook", - "in": "body" + } }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "Email", - "in": "body" + "400": { + "description": "Bad request — missing required field or invalid input" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "logsToInclude", - "in": "body" + "401": { + "description": "Unauthorized — invalid or missing bearer token" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "Severity", - "in": "body" + "403": { + "description": "Forbidden — caller lacks the required RBAC role" }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.SpamFilter.Read", + "parameters": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "onePerTenant", - "in": "body" - }, + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListTenantDetails": { + "get": { + "summary": "ListTenantDetails", + "tags": [ + "Tenant > Administration > Tenant" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "sendtoIntegration", - "in": "body" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" - } - } - } - }, - "/ExecSAMSetup": { - "post": { - "description": "ExecSAMSetup", - "summary": "ExecSAMSetup", - "tags": ["POST"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "error_description", - "in": "body" - }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "setkeys", - "in": "body" - }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "applicationsecret", - "in": "body" + } }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "error", - "in": "body" + "400": { + "description": "Bad request — missing required field or invalid input" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "CheckSetupProcess", - "in": "body" + "401": { + "description": "Unauthorized — invalid or missing bearer token" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "count", - "in": "body" + "403": { + "description": "Forbidden — caller lacks the required RBAC role" }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Core.Read", + "parameters": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "RefreshToken", - "in": "body" - }, + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListTenantDrift": { + "get": { + "summary": "ListTenantDrift", + "tags": [ + "Tenant > Standards" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "applicationid", - "in": "body" + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "step", - "in": "body" + "400": { + "description": "Bad request — missing required field or invalid input" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "tenantid", - "in": "body" + "401": { + "description": "Unauthorized — invalid or missing bearer token" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "partnersetup", - "in": "body" + "403": { + "description": "Forbidden — caller lacks the required RBAC role" }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Standards.Read", + "parameters": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "code", - "in": "body" - }, + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListTenantGroups": { + "post": { + "summary": "Entrypoint for listing tenant groups", + "tags": [ + "CIPP > Settings" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "CreateSAM", - "in": "body" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - }, - "get": { - "description": "ExecSAMSetup", - "summary": "ExecSAMSetup", - "tags": ["GET"], + }, + "x-cipp-role": "CIPP.Core.Read", "parameters": [ { - "required": true, + "name": "groupId", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "error_description", - "in": "query" + } }, { - "required": true, + "name": "includeUsage", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "setkeys", - "in": "query" - }, + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "groupId": { + "type": "string" + }, + "includeUsage": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ListTenantOnboarding": { + "get": { + "summary": "ListTenantOnboarding", + "tags": [ + "Tenant > Administration" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "applicationsecret", - "in": "query" + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "error", - "in": "query" + "400": { + "description": "Bad request — missing required field or invalid input" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "CheckSetupProcess", - "in": "query" + "401": { + "description": "Unauthorized — invalid or missing bearer token" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "count", - "in": "query" + "403": { + "description": "Forbidden — caller lacks the required RBAC role" }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Administration.Read", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "standardsExcludeAllTenants": { + "type": "string" + }, + "id": { + "type": "string" + }, + "remapRoles": { + "type": "string" + }, + "gdapRoles": { + "type": "string" + }, + "ignoreMissingRoles": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ListTenants": { + "post": { + "summary": "ListTenants", + "tags": [ + "Tenant > Administration > Tenant" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "RefreshToken", - "in": "query" + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "applicationid", - "in": "query" + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Core.Read", + "parameters": [ { - "required": true, + "name": "AllTenantSelector", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "step", - "in": "query" + } }, { - "required": true, + "name": "IncludeOffboardingDefaults", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "tenantid", - "in": "query" + } }, { - "required": true, + "name": "Mode", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "partnersetup", - "in": "query" + } }, { - "required": true, + "name": "TriggerRefresh", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "code", - "in": "query" + } }, { - "required": true, - "schema": { - "type": "string" - }, - "name": "CreateSAM", - "in": "query" + "$ref": "#/components/parameters/tenantFilter" } ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ClearCache": { + "type": "string" + }, + "integrationCompany": { + "$ref": "#/components/schemas/LabelValue" + }, + "TenantsOnly": { + "type": "string" + } } } - }, - "description": "Successful operation" + } } } } }, - "/ListUserMailboxDetails": { + "/api/ListTestReports": { "get": { - "description": "ListUserMailboxDetails", - "summary": "ListUserMailboxDetails", - "tags": ["GET"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" - }, + "summary": "Lists all available test reports from JSON files and database", + "tags": [ + "Uncategorized" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "UserID", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } + }, + "x-cipp-role": "Tenant.Reports.Read" } }, - "/ListExConnectorTemplates": { - "get": { - "description": "ListExConnectorTemplates", - "summary": "ListExConnectorTemplates", - "tags": ["GET"], - "parameters": [ + "/api/ListTests": { + "post": { + "summary": "Lists tests for a tenant, optionally filtered by report ID", + "tags": [ + "Uncategorized" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "ID", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" - } - } - } - }, - "/AddDefenderDeployment": { - "post": { - "description": "AddDefenderDeployment", - "summary": "AddDefenderDeployment", - "tags": ["POST"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "ASR", - "in": "body" + } }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "Compliance", - "in": "body" + "400": { + "description": "Bad request — missing required field or invalid input" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "EDR", - "in": "body" + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Reports.Read", + "parameters": [ { - "required": true, + "name": "reportId", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "selectedTenants", - "in": "body" + } }, { - "required": true, - "schema": { - "type": "string" - }, - "name": "Policy", - "in": "body" + "$ref": "#/components/parameters/tenantFilter" } ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" - } + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "reportId": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] } - }, - "description": "Successful operation" + } } } } }, - "/ListGDAPInvite": { + "/api/ListTransportRules": { "get": { - "description": "ListGDAPInvite", - "summary": "ListGDAPInvite", - "tags": ["GET"], - "parameters": [ + "summary": "ListTransportRules", + "tags": [ + "Email-Exchange > Transport" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "RelationshipId", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" - } - } - } - }, - "/AddIntuneTemplate": { - "post": { - "description": "AddIntuneTemplate", - "summary": "AddIntuneTemplate", - "tags": ["POST"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "TemplateType", - "in": "body" + } }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "description", - "in": "body" + "400": { + "description": "Bad request — missing required field or invalid input" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "body" + "401": { + "description": "Unauthorized — invalid or missing bearer token" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "displayname", - "in": "body" + "403": { + "description": "Forbidden — caller lacks the required RBAC role" }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.TransportRule.Read", + "parameters": [ { - "required": true, + "name": "id", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "rawJSON", - "in": "body" + } }, { - "required": true, - "schema": { - "type": "string" - }, - "name": "id", - "in": "body" - }, + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListTransportRulesTemplates": { + "get": { + "summary": "ListTransportRulesTemplates", + "tags": [ + "Email-Exchange > Transport" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "URLName", - "in": "body" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" - } - } - }, - "get": { - "description": "AddIntuneTemplate", - "summary": "AddIntuneTemplate", - "tags": ["GET"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "TemplateType", - "in": "query" - }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "description", - "in": "query" + } }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" + "400": { + "description": "Bad request — missing required field or invalid input" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "displayname", - "in": "query" + "401": { + "description": "Unauthorized — invalid or missing bearer token" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "rawJSON", - "in": "query" + "403": { + "description": "Forbidden — caller lacks the required RBAC role" }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.TransportRule.Read", + "parameters": [ { - "required": true, - "schema": { - "type": "string" - }, "name": "id", - "in": "query" - }, - { - "required": true, + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "URLName", - "in": "query" + } + } + ] + } + }, + "/api/ListUserConditionalAccessPolicies": { + "get": { + "summary": "ListUserConditionalAccessPolicies", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ + { + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/AddAPDevice": { - "post": { - "description": "AddAPDevice", - "summary": "AddAPDevice", - "tags": ["POST"], + }, + "x-cipp-role": "Identity.User.Read", "parameters": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "body" - }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "autopilotData", - "in": "body" + "$ref": "#/components/parameters/tenantFilter" }, { - "required": true, + "name": "UserID", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "Groupname", - "in": "body" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" - } - } - }, - "description": "Successful operation" + } } - } + ] } }, - "/RemoveTransportRuleTemplate": { + "/api/ListUserCounts": { "get": { - "description": "RemoveTransportRuleTemplate", - "summary": "RemoveTransportRuleTemplate", - "tags": ["GET"], - "parameters": [ + "summary": "ListUserCounts", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "id", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } + }, + "x-cipp-role": "Identity.User.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] } }, - "/ListMailboxMobileDevices": { + "/api/ListUserDevices": { "get": { - "description": "ListMailboxMobileDevices", - "summary": "ListMailboxMobileDevices", - "tags": ["GET"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" - }, + "summary": "ListUserDevices", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "Mailbox", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" - } - } - } - }, - "/ExecExcludeTenant": { - "post": { - "description": "ExecExcludeTenant", - "summary": "ExecExcludeTenant", - "tags": ["POST"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "ListAll", - "in": "body" + } }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "body" + "400": { + "description": "Bad request — missing required field or invalid input" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "AddExclusion", - "in": "body" + "401": { + "description": "Unauthorized — invalid or missing bearer token" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "value", - "in": "body" + "403": { + "description": "Forbidden — caller lacks the required RBAC role" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "RemoveExclusion", - "in": "body" + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Identity.User.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" }, { - "required": true, + "name": "UserID", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "List", - "in": "body" + } + } + ] + } + }, + "/api/ListUserGroups": { + "get": { + "summary": "ListUserGroups", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ + { + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" - } - } - }, - "get": { - "description": "ExecExcludeTenant", - "summary": "ExecExcludeTenant", - "tags": ["GET"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "ListAll", - "in": "query" + } }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" + "400": { + "description": "Bad request — missing required field or invalid input" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "AddExclusion", - "in": "query" + "401": { + "description": "Unauthorized — invalid or missing bearer token" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "value", - "in": "query" + "403": { + "description": "Forbidden — caller lacks the required RBAC role" }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Identity.User.Read", + "parameters": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "RemoveExclusion", - "in": "query" + "$ref": "#/components/parameters/tenantFilter" }, { - "required": true, + "name": "userId", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "List", - "in": "query" + } + } + ] + } + }, + "/api/ListUserMailboxDetails": { + "get": { + "summary": "ListUserMailboxDetails", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ + { + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/ExecSetSecurityAlert": { - "get": { - "description": "ExecSetSecurityAlert", - "summary": "ExecSetSecurityAlert", - "tags": ["GET"], + }, + "x-cipp-role": "Exchange.Mailbox.Read", "parameters": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" - }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "vendor", - "in": "query" + "$ref": "#/components/parameters/tenantFilter" }, { - "required": true, + "name": "UserID", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "GUID", - "in": "query" + } }, { - "required": true, + "name": "userMail", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "provider", - "in": "query" - }, + } + } + ] + } + }, + "/api/ListUserMailboxRules": { + "get": { + "summary": "ListUserMailboxRules", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "Status", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/ExecAddGDAPRole": { - "post": { - "description": "ExecAddGDAPRole", - "summary": "ExecAddGDAPRole", - "tags": ["POST"], + }, + "x-cipp-role": "Exchange.Mailbox.Read", + "x-cipp-dynamic-options": true, "parameters": [ { - "required": true, + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "userEmail", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "gdapRoles", - "in": "body" + } }, { - "required": true, + "name": "UserID", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "customSuffix", - "in": "body" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" - } - } - }, - "description": "Successful operation" + } } - } + ] } }, - "/ListExchangeConnectors": { + "/api/ListUserPhoto": { "get": { - "description": "ListExchangeConnectors", - "summary": "ListExchangeConnectors", - "tags": ["GET"], - "parameters": [ + "summary": "ListUserPhoto", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "tenantfilter", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/AddTransportTemplate": { - "post": { - "description": "AddTransportTemplate", - "summary": "AddTransportTemplate", - "tags": ["POST"], + }, + "x-cipp-role": "Identity.User.Read", "parameters": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "PowerShellCommand", - "in": "body" + "$ref": "#/components/parameters/tenantFilter" }, { - "required": true, + "name": "UserID", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "name", - "in": "body" + } + } + ] + } + }, + "/api/ListUserSettings": { + "get": { + "summary": "ListUserSettings", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ + { + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } + }, + "x-cipp-role": "Identity.User.Read" } }, - "/ExecCreateTAP": { + "/api/ListUserSigninLogs": { "get": { - "description": "ExecCreateTAP", - "summary": "ExecCreateTAP", - "tags": ["GET"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "ID", - "in": "query" - }, + "summary": "ListUserSigninLogs", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "tenantfilter", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/RemoveCAPolicy": { - "get": { - "description": "RemoveCAPolicy", - "summary": "RemoveCAPolicy", - "tags": ["GET"], + }, + "x-cipp-role": "Identity.User.Read", "parameters": [ { - "required": true, + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "top", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "TenantFilter", - "in": "query" + } }, { - "required": true, + "name": "UserID", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "GUID", - "in": "query" + } + } + ] + } + }, + "/api/ListUserTrustedBlockedSenders": { + "get": { + "summary": "ListUserTrustedBlockedSenders", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ + { + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/RemoveTransportRule": { - "get": { - "description": "RemoveTransportRule", - "summary": "RemoveTransportRule", - "tags": ["GET"], + }, + "x-cipp-role": "Exchange.Mailbox.Read", "parameters": [ { - "required": true, + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "UserID", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "tenantfilter", - "in": "query" + } }, { - "required": true, + "name": "userPrincipalName", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "guid", - "in": "query" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" - } - } - }, - "description": "Successful operation" + } } - } + ] } }, - "/RemoveAPDevice": { + "/api/ListUsers": { "get": { - "description": "RemoveAPDevice", - "summary": "RemoveAPDevice", - "tags": ["GET"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" - }, + "summary": "ListUsers", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "ID", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/AddPolicy": { - "post": { - "description": "AddPolicy", - "summary": "AddPolicy", - "tags": ["POST"], + }, + "x-cipp-role": "Identity.User.Read", "parameters": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "TemplateType", - "in": "body" - }, - { - "required": true, + "name": "graphFilter", + "in": "query", + "required": false, "schema": { - "type": "string" - }, - "name": "Description", - "in": "body" + "type": "string", + "description": "OData $filter expression passed to Graph" + } }, { - "required": true, + "name": "IncludeLogonDetails", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "Displayname", - "in": "body" + } }, { - "required": true, - "schema": { - "type": "string" - }, - "name": "replacemap", - "in": "body" + "$ref": "#/components/parameters/tenantFilter" }, { - "required": true, + "name": "UserID", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "RawJSON", - "in": "body" - }, + } + } + ] + } + }, + "/api/ListUsersAndGroups": { + "get": { + "summary": "ListUsersAndGroups", + "tags": [ + "Uncategorized" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "Assignto", - "in": "body" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } + }, + "x-cipp-role": "Tenant.Directory.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] } }, - "/AddContact": { - "post": { - "description": "AddContact", - "summary": "AddContact", - "tags": ["POST"], - "parameters": [ + "/api/ListWebhookAlert": { + "get": { + "summary": "ListWebhookAlert", + "tags": [ + "Tenant > Administration > Alerts" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "tenantid", - "in": "body" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } + }, + "x-cipp-role": "CIPP.Alert.Read" } }, - "/PublicScripts": { + "/api/ListmailboxPermissions": { "get": { - "description": "PublicScripts", - "summary": "PublicScripts", - "tags": ["GET"], - "parameters": [ + "summary": "ListmailboxPermissions", + "tags": [ + "Email-Exchange > Administration" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "Guid", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/RemoveContact": { - "get": { - "description": "RemoveContact", - "summary": "RemoveContact", - "tags": ["GET"], + }, + "x-cipp-role": "Exchange.Mailbox.Read", "parameters": [ { - "required": true, + "name": "ByUser", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "tenantfilter", - "in": "query" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" }, { - "required": true, + "name": "UseReportDB", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "guid", - "in": "query" + } + }, + { + "name": "userId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/PatchUser": { + "patch": { + "summary": "PatchUser", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ + { + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Identity.User.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true, + "properties": { + "id": { + "type": "string" + }, + "tenantFilter": { + "type": "string", + "description": "Required field on each array element (validated in handler)." + } + }, + "required": [ + "tenantFilter" + ] + } + }, + { + "type": "object", + "additionalProperties": true, + "properties": { + "id": { + "type": "string" + }, + "tenantFilter": { + "type": "string", + "description": "Required field on each array element (validated in handler)." + } + }, + "required": [ + "tenantFilter" + ] + } + ], + "description": "Accepts a single object or an array of objects. Each object must include the required fields." + } + } } } } }, - "/EditTransportRule": { - "get": { - "description": "EditTransportRule", - "summary": "EditTransportRule", - "tags": ["GET"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "tenantfilter", - "in": "query" - }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "guid", - "in": "query" - }, + "/api/PublicPhishingCheck": { + "post": { + "summary": "PublicPhishingCheck", + "tags": [ + "Uncategorized" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "state", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Public", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "AlertMessage": { + "type": "string" + }, + "Cloned": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "score": { + "type": "string" + }, + "source": { + "type": "string" + }, + "TenantId": { + "type": "string" + }, + "threshold": { + "type": "string" + }, + "type": { + "type": "string" + }, + "url": { + "type": "string" + }, + "userDisplayName": { + "type": "string" + }, + "userEmail": { + "type": "string" + } + } + } + } } } } }, - "/AddSpamFilter": { - "post": { - "description": "AddSpamFilter", - "summary": "AddSpamFilter", - "tags": ["POST"], - "parameters": [ + "/api/PublicPing": { + "get": { + "summary": "PublicPing", + "tags": [ + "CIPP > Core" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "PowerShellCommand", - "in": "body" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } + }, + "x-cipp-role": "Public" } }, - "/RemoveIntuneTemplate": { - "get": { - "description": "RemoveIntuneTemplate", - "summary": "RemoveIntuneTemplate", - "tags": ["GET"], - "parameters": [ + "/api/PublicWebhooks": { + "post": { + "summary": "PublicWebhooks", + "tags": [ + "Tenant > Administration > Alerts" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "id", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/ExecGroupsDeliveryManagement": { - "get": { - "description": "ExecGroupsDeliveryManagement", - "summary": "ExecGroupsDeliveryManagement", - "tags": ["GET"], + }, + "x-cipp-role": "Public", "parameters": [ { - "required": true, + "name": "CIPPID", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "GroupType", - "in": "query" + } }, { - "required": true, + "name": "Type", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "TenantFilter", - "in": "query" + } }, { - "required": true, + "name": "validationCode", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "id", - "in": "query" + } }, { - "required": true, + "name": "ValidationToken", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "OnlyAllowInternal", - "in": "query" + } } ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "validationCode": { + "type": "string" + }, + "value": { + "type": "string" + } } } - }, - "description": "Successful operation" + } } } } }, - "/ListUserDevices": { - "get": { - "description": "ListUserDevices", - "summary": "ListUserDevices", - "tags": ["GET"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" - }, + "/api/RemoveAPDevice": { + "post": { + "summary": "RemoveAPDevice", + "tags": [ + "Endpoint > Autopilot" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "UserID", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" - } - } - } - }, - "/EditTenant": { - "post": { - "description": "EditTenant", - "summary": "EditTenant", - "tags": ["POST"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "displayName", - "in": "body" + } }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "defaultDomainName", - "in": "body" + "400": { + "description": "Bad request — missing required field or invalid input" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "tenantid", - "in": "body" + "401": { + "description": "Unauthorized — invalid or missing bearer token" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "customerId", - "in": "body" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" - } - } - }, - "description": "Successful operation" + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/ExecAppApproval": { - "get": { - "description": "ExecAppApproval", - "summary": "ExecAppApproval", - "tags": ["GET"], + }, + "x-cipp-role": "Endpoint.Autopilot.ReadWrite", "parameters": [ { - "required": true, + "name": "ID", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "applicationid", - "in": "query" + } }, { - "required": true, - "schema": { - "type": "string" - }, - "name": "ID", - "in": "query" + "$ref": "#/components/parameters/tenantFilter" } ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" - } + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ID": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] } - }, - "description": "Successful operation" + } } } } }, - "/ListInactiveAccounts": { - "get": { - "description": "ListInactiveAccounts", - "summary": "ListInactiveAccounts", - "tags": ["GET"], - "parameters": [ + "/api/RemoveApp": { + "post": { + "summary": "RemoveApp", + "tags": [ + "Endpoint > Applications" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" + "bearerAuth": [] } ], "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" - } - } - }, - "description": "Successful operation" - } - } - } - }, - "/AddAlert": { - "post": { - "description": "AddAlert", - "summary": "AddAlert", - "tags": ["POST"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "ApnCertExpiry", - "in": "body" - }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "NewRole", - "in": "body" - }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "MFAAlertUsers", - "in": "body" - }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "DefenderMalware", - "in": "body" - }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "NewGA", - "in": "body" - }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "QuotaUsed", - "in": "body" - }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "AppSecretExpiry", - "in": "body" - }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "OverusedLicenses", - "in": "body" - }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "VppTokenExpiry", - "in": "body" - }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "SecDefaultsUpsell", - "in": "body" - }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "NoCAConfig", - "in": "body" - }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "DefenderStatus", - "in": "body" + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "AdminPassword", - "in": "body" + "400": { + "description": "Bad request — missing required field or invalid input" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "MFAAdmins", - "in": "body" + "401": { + "description": "Unauthorized — invalid or missing bearer token" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "DepTokenExpiry", - "in": "body" + "403": { + "description": "Forbidden — caller lacks the required RBAC role" }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.Application.ReadWrite", + "parameters": [ { - "required": true, + "name": "ID", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "SharePointQuota", - "in": "body" + } }, { - "required": true, - "schema": { - "type": "string" - }, - "name": "ExpiringLicenses", - "in": "body" - }, + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ID": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/RemoveAssignmentFilterTemplate": { + "post": { + "summary": "RemoveAssignmentFilterTemplate", + "tags": [ + "Endpoint > MEM" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "UnusedLicenses", - "in": "body" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/ListMailboxes": { - "get": { - "description": "ListMailboxes", - "summary": "ListMailboxes", - "tags": ["GET"], + }, + "x-cipp-role": "Endpoint.MEM.ReadWrite", "parameters": [ { - "required": true, + "name": "ID", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "TenantFilter", - "in": "query" + } } ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ID": { + "type": "string" + } } } - }, - "description": "Successful operation" + } } } } }, - "/ListMFAUsers": { - "get": { - "description": "ListMFAUsers", - "summary": "ListMFAUsers", - "tags": ["GET"], - "parameters": [ + "/api/RemoveAutopilotConfig": { + "post": { + "summary": "RemoveAutopilotConfig", + "tags": [ + "Endpoint > Autopilot" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.Autopilot.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "assignments": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "ID": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } } } } }, - "/RemoveGroupTemplate": { - "get": { - "description": "RemoveGroupTemplate", - "summary": "RemoveGroupTemplate", - "tags": ["GET"], - "parameters": [ + "/api/RemoveBPATemplate": { + "post": { + "summary": "RemoveBPATemplate", + "tags": [ + "Tenant > Standards" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "id", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/ExecRunBackup": { - "get": { - "description": "ExecRunBackup", - "summary": "ExecRunBackup", - "tags": ["GET"], + }, + "x-cipp-role": "Tenant.Standards.ReadWrite", "parameters": [ { - "required": true, + "name": "TemplateName", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "Selected", - "in": "query" + } } ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "TemplateName": { + "type": "string" + } } } - }, - "description": "Successful operation" + } } } } }, - "/ListTransportRules": { - "get": { - "description": "ListTransportRules", - "summary": "ListTransportRules", - "tags": ["GET"], - "parameters": [ + "/api/RemoveCAPolicy": { + "post": { + "summary": "RemoveCAPolicy", + "tags": [ + "Tenant > Conditional" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "tenantfilter", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/ExecMaintenanceScripts": { - "get": { - "description": "ExecMaintenanceScripts", - "summary": "ExecMaintenanceScripts", - "tags": ["GET"], + }, + "x-cipp-role": "Tenant.ConditionalAccess.ReadWrite", "parameters": [ { - "required": true, + "name": "GUID", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "MakeLink", - "in": "query" + } }, { - "required": true, - "schema": { - "type": "string" - }, - "name": "ScriptFile", - "in": "query" + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "GUID": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/RemoveCATemplate": { + "post": { + "summary": "RemoveCATemplate", + "tags": [ + "Tenant > Conditional" + ], + "security": [ + { + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.ConditionalAccess.ReadWrite", + "parameters": [ + { + "name": "ID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ID": { + "type": "string" + } } } - }, - "description": "Successful operation" + } } } } }, - "/GetCippAlerts": { - "get": { - "description": "GetCippAlerts", - "summary": "GetCippAlerts", - "tags": ["GET"], - "parameters": [ + "/api/RemoveConnectionfilterTemplate": { + "post": { + "summary": "RemoveConnectionfilterTemplate", + "tags": [ + "Email-Exchange > Spamfilter" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "localversion", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.ConnectionFilter.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ID": { + "type": "string" + } + } + } + } } } } }, - "/ExecGDAPMigration": { + "/api/RemoveContact": { "post": { - "description": "ExecGDAPMigration", - "summary": "ExecGDAPMigration", - "tags": ["POST"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "selectedTenants", - "in": "body" - }, + "summary": "RemoveContact", + "tags": [ + "Email-Exchange > Administration > Contacts" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "gdapRoles", - "in": "body" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/ListGroups": { - "get": { - "description": "ListGroups", - "summary": "ListGroups", - "tags": ["GET"], + }, + "x-cipp-role": "Exchange.Contact.ReadWrite", "parameters": [ { - "required": true, + "name": "GUID", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "GroupID", - "in": "query" + } }, { - "required": true, + "name": "Mail", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "TenantFilter", - "in": "query" + } }, { - "required": true, - "schema": { - "type": "string" - }, - "name": "owners", - "in": "query" - }, + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "GUID": { + "type": "string" + }, + "Mail": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/RemoveContactTemplates": { + "post": { + "summary": "RemoveContactTemplates", + "tags": [ + "Email-Exchange > Administration > Contacts" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "members", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/ListDomains": { - "get": { - "description": "ListDomains", - "summary": "ListDomains", - "tags": ["GET"], + }, + "x-cipp-role": "Exchange.Contact.ReadWrite", "parameters": [ { - "required": true, + "name": "ID", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "TenantFilter", - "in": "query" + } } ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ID": { + "type": "string" + } } } - }, - "description": "Successful operation" + } } } } }, - "/ListExternalTenantInfo": { - "get": { - "description": "ListExternalTenantInfo", - "summary": "ListExternalTenantInfo", - "tags": ["GET"], - "parameters": [ + "/api/RemoveDeletedObject": { + "post": { + "summary": "RemoveDeletedObject", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "tenant", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/ListAPDevices": { - "get": { - "description": "ListAPDevices", - "summary": "ListAPDevices", - "tags": ["GET"], + }, + "x-cipp-role": "Tenant.Directory.ReadWrite", "parameters": [ { - "required": true, + "name": "ID", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "TenantFilter", - "in": "query" + } }, { - "required": true, - "schema": { - "type": "string" - }, - "name": "UserID", - "in": "query" + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "displayName": { + "type": "string" + }, + "ID": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "userPrincipalName": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/RemoveExConnector": { + "post": { + "summary": "RemoveExConnector", + "tags": [ + "Email-Exchange > Transport" + ], + "security": [ + { + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/AddAutopilotConfig": { - "post": { - "description": "AddAutopilotConfig", - "summary": "AddAutopilotConfig", - "tags": ["POST"], + }, + "x-cipp-role": "Exchange.Connector.ReadWrite", "parameters": [ { - "required": true, + "name": "GUID", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "Displayname", - "in": "body" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" }, { - "required": true, + "name": "Type", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "Description", - "in": "body" - }, + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "GUID": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "Type": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/RemoveExConnectorTemplate": { + "post": { + "summary": "RemoveExConnectorTemplate", + "tags": [ + "Email-Exchange > Transport" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "Assignto", - "in": "body" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/AddCAPolicy": { - "post": { - "description": "AddCAPolicy", - "summary": "AddCAPolicy", - "tags": ["POST"], + }, + "x-cipp-role": "Exchange.Connector.ReadWrite", "parameters": [ { - "required": true, + "name": "ID", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "RawJSON", - "in": "body" + } } ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ID": { + "type": "string" + } } } - }, - "description": "Successful operation" + } } } } }, - "/ListMailboxStatistics": { - "get": { - "description": "ListMailboxStatistics", - "summary": "ListMailboxStatistics", - "tags": ["GET"], - "parameters": [ + "/api/RemoveGroupTemplate": { + "post": { + "summary": "RemoveGroupTemplate", + "tags": [ + "Identity > Administration > Groups" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/ExecAssignApp": { - "get": { - "description": "ExecAssignApp", - "summary": "ExecAssignApp", - "tags": ["GET"], + }, + "x-cipp-role": "Identity.Group.ReadWrite", "parameters": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" - }, - { - "required": true, - "schema": { - "type": "string" - }, "name": "ID", - "in": "query" - }, - { - "required": true, + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "AssignTo", - "in": "query" + } } ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ID": { + "type": "string" + } } } - }, - "description": "Successful operation" + } } } } }, - "/ExecExtensionTest": { - "get": { - "description": "ExecExtensionTest", - "summary": "ExecExtensionTest", - "tags": ["GET"], - "parameters": [ + "/api/RemoveIntuneReusableSetting": { + "post": { + "summary": "RemoveIntuneReusableSetting", + "tags": [ + "Endpoint > MEM" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "extensionName", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" - } - } - } - }, - "/ExecSetMailboxQuota": { - "post": { - "description": "ExecSetMailboxQuota", - "summary": "ExecSetMailboxQuota", - "tags": ["POST"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "input", - "in": "body" + } }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "ProhibitSendReceiveQuota", - "in": "body" + "400": { + "description": "Bad request — missing required field or invalid input" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "ProhibitSendQuota", - "in": "body" + "401": { + "description": "Unauthorized — invalid or missing bearer token" }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.MEM.ReadWrite", + "parameters": [ { - "required": true, + "name": "DisplayName", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "tenantfilter", - "in": "body" + } }, { - "required": true, + "name": "ID", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "IssueWarningQuota", - "in": "body" + } }, { - "required": true, - "schema": { - "type": "string" - }, - "name": "user", - "in": "body" + "$ref": "#/components/parameters/tenantFilter" } ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" - } + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "DisplayName": { + "type": "string" + }, + "ID": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] } - }, - "description": "Successful operation" + } } } } }, - "/ListNamedLocations": { - "get": { - "description": "ListNamedLocations", - "summary": "ListNamedLocations", - "tags": ["GET"], - "parameters": [ + "/api/RemoveIntuneReusableSettingTemplate": { + "post": { + "summary": "RemoveIntuneReusableSettingTemplate", + "tags": [ + "Endpoint > MEM" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "TenantFilter", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/ListMFAUsersAllTenants": { - "get": { - "description": "ListMFAUsersAllTenants", - "summary": "ListMFAUsersAllTenants", - "tags": ["GET"], + }, + "x-cipp-role": "Endpoint.MEM.ReadWrite", "parameters": [ { - "required": true, + "name": "ID", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "TenantFilter", - "in": "query" + } } ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ID": { + "type": "string" + } } } - }, - "description": "Successful operation" + } } } } }, - "/RemoveQueuedAlert": { - "get": { - "description": "RemoveQueuedAlert", - "summary": "RemoveQueuedAlert", - "tags": ["GET"], - "parameters": [ + "/api/RemoveIntuneScript": { + "post": { + "summary": "RemoveIntuneScript", + "tags": [ + "Endpoint > MEM" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "id", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.MEM.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "DisplayName": { + "type": "string" + }, + "ID": { + "type": "string" + }, + "ScriptType": { + "type": "string" + }, + "TenantFilter": { + "type": "string" + } + }, + "required": [ + "TenantFilter" + ] + } + } } } } }, - "/ExecAccessChecks": { + "/api/RemoveIntuneTemplate": { "post": { - "description": "ExecAccessChecks", - "summary": "ExecAccessChecks", - "tags": ["POST"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "Tenants", - "in": "body" - }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "tenantid", - "in": "body" - }, + "summary": "RemoveIntuneTemplate", + "tags": [ + "Endpoint > MEM" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "Permissions", - "in": "body" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" - } - } - }, - "get": { - "description": "ExecAccessChecks", - "summary": "ExecAccessChecks", - "tags": ["GET"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "Tenants", - "in": "query" + } }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "tenantid", - "in": "query" + "400": { + "description": "Bad request — missing required field or invalid input" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "Permissions", - "in": "query" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" - } - } - }, - "description": "Successful operation" + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/ListSharedMailboxAccountEnabled": { - "get": { - "description": "ListSharedMailboxAccountEnabled", - "summary": "ListSharedMailboxAccountEnabled", - "tags": ["GET"], + }, + "x-cipp-role": "Endpoint.MEM.ReadWrite", "parameters": [ { - "required": true, + "name": "ID", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "TenantFilter", - "in": "query" + } } ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ID": { + "type": "string" + } } } - }, - "description": "Successful operation" + } } } } }, - "/ListSpamfilter": { - "get": { - "description": "ListSpamfilter", - "summary": "ListSpamfilter", - "tags": ["GET"], - "parameters": [ + "/api/RemoveJITAdminTemplate": { + "post": { + "summary": "RemoveJITAdminTemplate", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "tenantfilter", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/ListQuarantinePolicy": { - "get": { - "description": "ListQuarantinePolicy", - "summary": "ListQuarantinePolicy", - "tags": ["GET"], + }, + "x-cipp-role": "Identity.Role.ReadWrite", "parameters": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "tenantfilter", - "in": "query" - }, - { - "required": true, + "name": "ID", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "Type", - "in": "query" + } } ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ID": { + "type": "string" + } } } - }, - "description": "Successful operation" + } } } } }, - "/AddQuarantinePolicy": { + "/api/RemovePolicy": { "post": { - "description": "AddQuarantinePolicy", - "summary": "AddQuarantinePolicy", - "tags": ["POST"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "tenantfilter", - "in": "query" - }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "Name", - "in": "body" - }, - { - "required": true, - "schema": { - "type": "boolean" - }, - "name": "BlockSender", - "in": "body" - }, - { - "required": true, - "schema": { - "type": "boolean" - }, - "name": "Delete", - "in": "body" - }, - { - "required": true, - "schema": { - "type": "boolean" - }, - "name": "Preview", - "in": "body" - }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "ReleaseActionPreference", - "in": "body" - }, - { - "required": true, - "schema": { - "type": "boolean" - }, - "name": "AllowSender", - "in": "body" - }, - { - "required": true, - "schema": { - "type": "boolean" - }, - "name": "QuarantineNotification", - "in": "body" - }, + "summary": "RemovePolicy", + "tags": [ + "Endpoint > MEM" + ], + "security": [ { - "required": true, - "schema": { - "type": "boolean" - }, - "name": "IncludeMessagesFromBlockedSenderAddress", - "in": "body" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" - } - } - } - }, - "/EditQuarantinePolicy": { - "post": { - "description": "EditQuarantinePolicy", - "summary": "EditQuarantinePolicy", - "tags": ["POST"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "tenantfilter", - "in": "query" - }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "Type", - "in": "query" + } }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "Identity", - "in": "body" + "400": { + "description": "Bad request — missing required field or invalid input" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "EndUserSpamNotificationFrequency", - "in": "body" + "401": { + "description": "Unauthorized — invalid or missing bearer token" }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "EndUserSpamNotificationCustomFromAddress", - "in": "body" + "403": { + "description": "Forbidden — caller lacks the required RBAC role" }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.MEM.ReadWrite", + "parameters": [ { + "name": "ID", + "in": "query", "required": false, - "schema": { - "type": "boolean" - }, - "name": "OrganizationBrandingEnabled", - "in": "body" - }, - { - "required": true, - "schema": { - "type": "boolean" - }, - "name": "BlockSender", - "in": "body" - }, - { - "required": true, - "schema": { - "type": "boolean" - }, - "name": "Delete", - "in": "body" - }, - { - "required": true, - "schema": { - "type": "boolean" - }, - "name": "Preview", - "in": "body" - }, - { - "required": true, "schema": { "type": "string" - }, - "name": "ReleaseActionPreference", - "in": "body" - }, - { - "required": true, - "schema": { - "type": "boolean" - }, - "name": "AllowSender", - "in": "body" + } }, { - "required": true, - "schema": { - "type": "boolean" - }, - "name": "QuarantineNotification", - "in": "body" + "$ref": "#/components/parameters/tenantFilter" }, { - "required": true, + "name": "URLName", + "in": "query", + "required": false, "schema": { - "type": "boolean" - }, - "name": "IncludeMessagesFromBlockedSenderAddress", - "in": "body" + "type": "string" + } } ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" - } + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ID": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "URLName": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] } - }, - "description": "Successful operation" + } } } } }, - "/RemoveExConnector": { - "get": { - "description": "RemoveExConnector", - "summary": "RemoveExConnector", - "tags": ["GET"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "tenantfilter", - "in": "query" - }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "guid", - "in": "query" - }, + "/api/RemoveQuarantinePolicy": { + "post": { + "summary": "RemoveQuarantinePolicy", + "tags": [ + "Email-Exchange > Spamfilter" + ], + "security": [ { - "required": true, - "schema": { - "type": "string" - }, - "name": "Type", - "in": "query" + "bearerAuth": [] } ], "responses": { "200": { + "description": "Success", "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "$ref": "#/components/schemas/StandardResults" } } - }, - "description": "Successful operation" + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - } - }, - "/RemoveQuarantinePolicy": { - "get": { - "description": "RemoveQuarantinePolicy", - "summary": "RemoveQuarantinePolicy", - "tags": ["GET"], + }, + "x-cipp-role": "Exchange.Spamfilter.ReadWrite", "parameters": [ { - "required": true, + "name": "Identity", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "tenantfilter", - "in": "query" + } }, { - "required": true, + "name": "Name", + "in": "query", + "required": false, "schema": { "type": "string" - }, - "name": "Identity", - "in": "body" + } }, { - "required": false, - "schema": { - "type": "string" - }, - "name": "Name", - "in": "body" + "$ref": "#/components/parameters/tenantFilter" } ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" - } - } - }, - "description": "Successful operation" - } - } - } - } - }, - "/ExecDeleteSafeLinksPolicy": { - "get": { - "description": "ExecDeleteSafeLinksPolicy", - "summary": "ExecDeleteSafeLinksPolicy", - "tags": ["GET"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "tenantFilter", - "in": "query" - }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "RuleName", - "in": "query" - }, - { + "requestBody": { "required": true, - "schema": { - "type": "string" - }, - "name": "PolicyName", - "in": "query" - } - ], - "responses": { - "200": { "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" - } - } - }, - "description": "Successful operation" - } - } - } - }, - "/EditSafeLinksPolicy": { - "post": { - "description": "EditSafeLinksPolicy", - "summary": "EditSafeLinksPolicy", - "tags": ["POST"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "tenantFilter", - "in": "query" - }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "PolicyName", - "in": "query" - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "Name": { - "type": "string" - }, - "EnableSafeLinksForEmail": { - "type": "boolean" - }, - "EnableSafeLinksForTeams": { - "type": "boolean" - }, - "EnableSafeLinksForOffice": { - "type": "boolean" - }, - "TrackClicks": { - "type": "boolean" - }, - "AllowClickThrough": { - "type": "boolean" - }, - "ScanUrls": { - "type": "boolean" - }, - "EnableForInternalSenders": { - "type": "boolean" - }, - "DeliverMessageAfterScan": { - "type": "boolean" - }, - "DisableUrlRewrite": { - "type": "boolean" - }, - "DoNotRewriteUrls": { - "type": "array", - "items": { - "type": "string" - } - }, - "AdminDisplayName": { - "type": "string" - }, - "CustomNotificationText": { - "type": "string" - }, - "EnableOrganizationBranding": { - "type": "boolean" - }, - "Priority": { - "type": "integer" - }, - "Comments": { - "type": "string" - }, - "Enabled": { - "type": "boolean" - }, - "SentTo": { - "type": "array", - "items": { - "type": "string" - } - }, - "ExceptIfSentTo": { - "type": "array", - "items": { - "type": "string" - } - }, - "SentToMemberOf": { - "type": "array", - "items": { + "type": "object", + "properties": { + "Identity": { "type": "string" - } - }, - "ExceptIfSentToMemberOf": { - "type": "array", - "items": { + }, + "Name": { "type": "string" - } - }, - "RecipientDomainIs": { - "type": "array", - "items": { + }, + "TenantFilter": { "type": "string" } }, - "ExceptIfRecipientDomainIs": { - "type": "array", - "items": { - "type": "string" - } - } + "required": [ + "TenantFilter" + ] } } } } - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "Results": { - "type": "array", - "items": { - "type": "string" - } - } + } + }, + "/api/RemoveQueuedAlert": { + "post": { + "summary": "RemoveQueuedAlert", + "tags": [ + "Tenant > Administration > Alerts" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" } } } }, - "description": "Successful operation" - } - } - } - }, - "/ListSafeLinksPolicyDetails": { - "get": { - "description": "ListSafeLinksPolicyDetails", - "summary": "ListSafeLinksPolicyDetails", - "tags": ["GET"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "tenantFilter", - "in": "query" - }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "PolicyName", - "in": "query" - }, - { - "required": false, - "schema": { - "type": "string" + "400": { + "description": "Bad request — missing required field or invalid input" }, - "name": "RuleName", - "in": "query" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "Results": { - "type": "object", - "properties": { - "Policy": { - "type": "object", - "properties": { - "Name": { - "type": "string" - }, - "EnableSafeLinksForEmail": { - "type": "boolean" - }, - "EnableSafeLinksForTeams": { - "type": "boolean" - }, - "EnableSafeLinksForOffice": { - "type": "boolean" - }, - "TrackClicks": { - "type": "boolean" - }, - "AllowClickThrough": { - "type": "boolean" - }, - "ScanUrls": { - "type": "boolean" - }, - "EnableForInternalSenders": { - "type": "boolean" - }, - "DeliverMessageAfterScan": { - "type": "boolean" - }, - "DisableUrlRewrite": { - "type": "boolean" - }, - "DoNotRewriteUrls": { - "type": "array", - "items": { - "type": "string" - } - }, - "AdminDisplayName": { - "type": "string" - }, - "CustomNotificationText": { - "type": "string" - }, - "EnableOrganizationBranding": { - "type": "boolean" - } - } - }, - "Rule": { - "type": "object", - "properties": { - "Name": { - "type": "string" - }, - "Priority": { - "type": "integer" - }, - "Comments": { - "type": "string" - }, - "State": { - "type": "string" - }, - "SentTo": { - "type": "array", - "items": { - "type": "string" - } - }, - "ExceptIfSentTo": { - "type": "array", - "items": { - "type": "string" - } - }, - "SentToMemberOf": { - "type": "array", - "items": { - "type": "string" - } - }, - "ExceptIfSentToMemberOf": { - "type": "array", - "items": { - "type": "string" - } - }, - "RecipientDomainIs": { - "type": "array", - "items": { - "type": "string" - } - }, - "ExceptIfRecipientDomainIs": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "Message": { - "type": "string" - } - } - } - } - } - } + "401": { + "description": "Unauthorized — invalid or missing bearer token" }, - "description": "Successful operation" - } - } - } - }, - "/ExecNewSafeLinksPolicy": { - "post": { - "description": "Create a new SafeLinks policy and associated rule", - "summary": "Create SafeLinks Policy and Rule Configuration", - "tags": ["POST"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" + "403": { + "description": "Forbidden — caller lacks the required RBAC role" }, - "name": "tenantFilter", - "in": "query" + "500": { + "description": "Internal server error" + } }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "Name", - "in": "query" - } - ], - "requestBody": { - "content": { - "application/json": { + "x-cipp-role": "CIPP.Alert.ReadWrite", + "parameters": [ + { + "name": "EventType", + "in": "query", + "required": false, "schema": { - "type": "object", - "properties": { - "EnableSafeLinksForEmail": { - "type": "boolean", - "description": "Enable Safe Links protection for email messages" - }, - "EnableSafeLinksForTeams": { - "type": "boolean", - "description": "Enable Safe Links protection for Teams messages" - }, - "EnableSafeLinksForOffice": { - "type": "boolean", - "description": "Enable Safe Links protection for Office applications" - }, - "TrackClicks": { - "type": "boolean", - "description": "Track user clicks related to Safe Links protection" - }, - "AllowClickThrough": { - "type": "boolean", - "description": "Allow users to click through to the original URL" - }, - "ScanUrls": { - "type": "boolean", - "description": "Enable real-time scanning of URLs" - }, - "EnableForInternalSenders": { - "type": "boolean", - "description": "Enable Safe Links for messages sent between internal senders" - }, - "DeliverMessageAfterScan": { - "type": "boolean", - "description": "Wait until URL scanning is complete before delivering the message" - }, - "DisableUrlRewrite": { - "type": "boolean", - "description": "Disable the rewriting of URLs in messages" - }, - "DoNotRewriteUrls": { - "type": "array", - "items": { - "type": "string" - }, - "description": "List of URLs that will not be rewritten by Safe Links" - }, - "AdminDisplayName": { - "type": "string", - "description": "Display name for the policy in the admin interface" - }, - "CustomNotificationText": { - "type": "string", - "description": "Custom text for the notification when a link is blocked" - }, - "EnableOrganizationBranding": { - "type": "boolean", - "description": "Enable organization branding on warning pages" - }, - "Priority": { - "type": "integer", - "description": "Priority of the rule (lower numbers = higher priority)" - }, - "Comments": { - "type": "string", - "description": "Administrative comments for the rule" - }, - "Enabled": { - "type": "boolean", - "description": "Whether the rule is enabled or disabled" - }, - "SentTo": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Apply the rule if the recipient is any of these email addresses" - }, - "SentToMemberOf": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Apply the rule if the recipient is a member of any of these groups" - }, - "RecipientDomainIs": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Apply the rule if the recipient's domain matches any of these domains" - }, - "ExceptIfSentTo": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Do not apply the rule if the recipient is any of these email addresses" - }, - "ExceptIfSentToMemberOf": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Do not apply the rule if the recipient is a member of any of these groups" - }, - "ExceptIfRecipientDomainIs": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Do not apply the rule if the recipient's domain matches any of these domains" - } - } + "type": "string" + } + }, + { + "name": "ID", + "in": "query", + "required": false, + "schema": { + "type": "string" } } - } - }, - "responses": { - "200": { + ], + "requestBody": { + "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { - "Results": { - "type": "string", - "description": "Result message from the operation" + "EventType": { + "type": "string" + }, + "ID": { + "type": "string" } } } } + } + } + } + }, + "/api/RemoveQueuedApp": { + "post": { + "summary": "RemoveQueuedApp", + "tags": [ + "Endpoint > Applications" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" }, - "description": "Successfully created SafeLinks policy and rule" + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } }, - "400": { - "description": "Bad request - missing required parameters", + "x-cipp-role": "Endpoint.Application.ReadWrite", + "requestBody": { + "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { - "Results": { - "type": "string", - "description": "Error message" + "ID": { + "type": "string" } } } } } + } + } + }, + "/api/RemoveSafeLinksPolicyTemplate": { + "post": { + "summary": "RemoveSafeLinksPolicyTemplate", + "tags": [ + "Security > Safe-Links-Policy" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } }, - "500": { - "description": "Internal server error", + "x-cipp-role": "Exchange.SafeLinks.ReadWrite", + "parameters": [ + { + "name": "ID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { - "Results": { - "type": "string", - "description": "Error message" + "ID": { + "type": "string" } } } @@ -9431,736 +33767,869 @@ } } } - } - }, - "/ListSafeLinksPolicyTemplates": { - "get": { - "description": "List SafeLinks Policy Templates", - "summary": "List SafeLinks Policy Templates", - "tags": ["GET"], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object", - "properties": {} + }, + "/api/RemoveScheduledItem": { + "post": { + "summary": "RemoveScheduledItem", + "tags": [ + "CIPP > Scheduler" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" } } } }, - "description": "Successful operation" - } - } - } - }, - "/RemoveSafeLinksPolicyTemplate": { - "get": { - "description": "Remove SafeLinks Policy Template", - "summary": "Remove SafeLinks Policy Template", - "tags": ["GET"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" + "400": { + "description": "Bad request — missing required field or invalid input" }, - "name": "id", - "in": "query" - } - ], - "responses": { - "200": { + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Removes a scheduled item from CIPP's scheduler.\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", + "x-cipp-role": "CIPP.Scheduler.ReadWrite", + "parameters": [ + { + "name": "id", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "type": "object", + "properties": { + "id": { + "type": "string" + } + } } } - }, - "description": "Successful operation" + } } } - } - }, - "/AddSafeLinksPolicyTemplate": { - "post": { - "description": "Add SafeLinks Policy Template", - "summary": "Add SafeLinks Policy Template", - "tags": ["POST"], - "requestBody": { - "content": { - "application/json": { + }, + "/api/RemoveSpamfilter": { + "post": { + "summary": "RemoveSpamfilter", + "tags": [ + "Email-Exchange > Spamfilter" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Spamfilter.ReadWrite", + "parameters": [ + { + "name": "name", + "in": "query", + "required": false, "schema": { - "type": "object", - "properties": {} + "type": "string" } + }, + { + "$ref": "#/components/parameters/tenantFilter" } - } - }, - "responses": { - "200": { + ], + "requestBody": { + "required": true, "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "type": "object", + "properties": { + "name": { + "type": "string" + } + } } } - }, - "description": "Successful operation" + } } } - } - }, - "/AddSafeLinksPolicyFromTemplate": { - "post": { - "description": "Deploy SafeLinks Policy From Template", - "summary": "Deploy SafeLinks Policy From Template", - "tags": ["POST"], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": {} + }, + "/api/RemoveSpamfilterTemplate": { + "post": { + "summary": "RemoveSpamfilterTemplate", + "tags": [ + "Email-Exchange > Spamfilter" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } - } - }, - "responses": { - "200": { + }, + "x-cipp-role": "Exchange.Spamfilter.ReadWrite", + "requestBody": { + "required": true, "content": { "application/json": { "schema": { - "properties": {}, - "type": "object" + "type": "object", + "properties": { + "ID": { + "type": "string" + } + } } } - }, - "description": "Successful operation" + } } } - } - }, - "/ExecManageRetentionPolicies": { - "get": { - "description": "List retention policies or get a specific retention policy by name", - "summary": "Get Retention Policies", - "tags": ["GET"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" + }, + "/api/RemoveStandard": { + "get": { + "summary": "RemoveStandard", + "tags": [ + "Tenant > Standards" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } }, - "name": "tenantFilter", - "in": "query" - }, - { - "required": false, - "schema": { - "type": "string" + "400": { + "description": "Bad request — missing required field or invalid input" }, - "name": "name", - "in": "query", - "description": "Name of specific retention policy to retrieve" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "Name": { - "type": "string" - }, - "RetentionPolicyTagLinks": { - "type": "array", - "items": { - "type": "string" - } - } - } + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Standards.ReadWrite", + "parameters": [ + { + "name": "ID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/RemoveStandardTemplate": { + "post": { + "summary": "RemoveStandardTemplate", + "tags": [ + "Tenant > Standards" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" } } } }, - "description": "Successfully retrieved retention policies" + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } }, - "400": { - "description": "Bad request - missing required parameters", + "x-cipp-role": "Tenant.Standards.ReadWrite", + "parameters": [ + { + "name": "ID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { - "Results": { - "type": "string", - "description": "Error message" + "ID": { + "type": "string" } } } } } + } + } + }, + "/api/RemoveTenantAllowBlockList": { + "post": { + "summary": "RemoveTenantAllowBlockList", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } }, - "500": { - "description": "Internal server error", + "x-cipp-role": "Exchange.SpamFilter.ReadWrite", + "requestBody": { + "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { - "Results": { - "type": "string", - "description": "Error message" + "Entries": { + "type": "string" + }, + "ListType": { + "type": "string" + }, + "tenantFilter": { + "type": "string" } - } + }, + "required": [ + "tenantFilter" + ] } } } } } }, - "post": { - "description": "Create, modify, or delete retention policies", - "summary": "Manage Retention Policies", - "tags": ["POST"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "tenantFilter", - "in": "query" - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "CreatePolicies": { - "type": "array", - "items": { - "type": "object", - "properties": { - "Name": { - "type": "string", - "description": "Name of the retention policy" - }, - "RetentionPolicyTagLinks": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Array of retention tag names to link to this policy" - } - }, - "required": ["Name"] - }, - "description": "Array of retention policies to create" - }, - "ModifyPolicies": { - "type": "array", - "items": { - "type": "object", - "properties": { - "Identity": { - "type": "string", - "description": "Identity of the retention policy to modify" - }, - "Name": { - "type": "string", - "description": "New name for the retention policy" - }, - "RetentionPolicyTagLinks": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Array of retention tag names to link to this policy" - }, - "AddTags": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Array of retention tag names to add to the policy" - }, - "RemoveTags": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Array of retention tag names to remove from the policy" - } - }, - "required": ["Identity"] - }, - "description": "Array of retention policies to modify" - }, - "DeletePolicies": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Array of retention policy identities to delete" - } - } - } + "/api/RemoveTenantCapabilitiesCache": { + "get": { + "summary": "RemoveTenantCapabilitiesCache", + "tags": [ + "Tenant > Administration > Tenant" + ], + "security": [ + { + "bearerAuth": [] } - } - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "string" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" } } } }, - "description": "Successfully processed retention policy operations" + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } }, - "400": { - "description": "Bad request - missing required parameters", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "string" + "x-cipp-role": "Tenant.Administration.ReadWrite", + "parameters": [ + { + "name": "defaultDomainName", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/RemoveTransportRule": { + "post": { + "summary": "RemoveTransportRule", + "tags": [ + "Email-Exchange > Transport" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" } } } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } }, - "403": { - "description": "Forbidden - insufficient permissions", + "x-cipp-role": "Exchange.TransportRule.ReadWrite", + "parameters": [ + { + "name": "guid", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, "content": { "application/json": { "schema": { - "type": "array", - "items": { - "type": "string" - } + "type": "object", + "properties": { + "guid": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] } } } } } - } - }, - "/ExecManageRetentionTags": { - "get": { - "description": "List retention tags or get a specific retention tag by name", - "summary": "Get Retention Tags", - "tags": ["GET"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "tenantFilter", - "in": "query" - }, - { - "required": false, - "schema": { - "type": "string" - }, - "name": "name", - "in": "query", - "description": "Name of specific retention tag to retrieve" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "Name": { - "type": "string" - }, - "Type": { - "type": "string" - }, - "RetentionAction": { - "type": "string" - }, - "AgeLimitForRetention": { - "type": "integer" - }, - "RetentionEnabled": { - "type": "boolean" - } - } + }, + "/api/RemoveTransportRuleTemplate": { + "post": { + "summary": "RemoveTransportRuleTemplate", + "tags": [ + "Email-Exchange > Transport" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" } } } }, - "description": "Successfully retrieved retention tags" + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } }, - "400": { - "description": "Bad request - missing required parameters", + "x-cipp-role": "Exchange.TransportRule.ReadWrite", + "parameters": [ + { + "name": "ID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { - "Results": { - "type": "string", - "description": "Error message" + "ID": { + "type": "string" } } } } } + } + } + }, + "/api/RemoveTrustedBlockedSender": { + "post": { + "summary": "RemoveTrustedBlockedSender", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } }, - "500": { - "description": "Internal server error", + "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "requestBody": { + "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { - "Results": { - "type": "string", - "description": "Error message" - } - } - } - } - } - } - } - }, - "post": { - "description": "Create, modify, or delete retention tags", - "summary": "Manage Retention Tags", - "tags": ["POST"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "tenantFilter", - "in": "query" - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "CreateTags": { - "type": "array", - "items": { - "type": "object", - "properties": { - "Name": { - "type": "string", - "description": "Name of the retention tag (max 64 characters)" - }, - "Type": { - "type": "string", - "enum": [ - "All", - "Inbox", - "SentItems", - "DeletedItems", - "Drafts", - "Outbox", - "JunkEmail", - "Journal", - "SyncIssues", - "ConversationHistory", - "Personal", - "RecoverableItems", - "NonIpmRoot", - "LegacyArchiveJournals", - "Clutter", - "Calendar", - "Notes", - "Tasks", - "Contacts", - "RssSubscriptions", - "ManagedCustomFolder" - ], - "description": "Type of the retention tag" - }, - "RetentionAction": { - "type": "string", - "enum": [ - "DeleteAndAllowRecovery", - "PermanentlyDelete", - "MoveToArchive", - "MarkAsPastRetentionLimit" - ], - "description": "Action to take when retention period expires" - }, - "AgeLimitForRetention": { - "type": "integer", - "minimum": 0, - "maximum": 24855, - "description": "Age limit for retention in days" - }, - "RetentionEnabled": { - "type": "boolean", - "description": "Whether retention is enabled for this tag" - }, - "Comment": { - "type": "string", - "description": "Administrative comment for the tag" - }, - "LocalizedComment": { - "type": "string", - "description": "Localized comment for the tag" - }, - "LocalizedRetentionPolicyTagName": { - "type": "string", - "description": "Localized name for the retention policy tag" - } - }, - "required": ["Name", "Type"] + "tenantFilter": { + "type": "string" }, - "description": "Array of retention tags to create" - }, - "ModifyTags": { - "type": "array", - "items": { - "type": "object", - "properties": { - "Identity": { - "type": "string", - "description": "Identity of the retention tag to modify" - }, - "Name": { - "type": "string", - "description": "New name for the retention tag" - }, - "RetentionAction": { - "type": "string", - "enum": [ - "DeleteAndAllowRecovery", - "PermanentlyDelete", - "MoveToArchive", - "MarkAsPastRetentionLimit" - ], - "description": "Action to take when retention period expires" - }, - "AgeLimitForRetention": { - "type": "integer", - "minimum": 0, - "maximum": 24855, - "description": "Age limit for retention in days" - }, - "RetentionEnabled": { - "type": "boolean", - "description": "Whether retention is enabled for this tag" - }, - "Comment": { - "type": "string", - "description": "Administrative comment for the tag" - }, - "LocalizedComment": { - "type": "string", - "description": "Localized comment for the tag" - }, - "LocalizedRetentionPolicyTagName": { - "type": "string", - "description": "Localized name for the retention policy tag" - } - }, - "required": ["Identity"] + "typeProperty": { + "type": "string" }, - "description": "Array of retention tags to modify" - }, - "DeleteTags": { - "type": "array", - "items": { + "userPrincipalName": { "type": "string" }, - "description": "Array of retention tag identities to delete" - } + "value": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] } } } } - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "string" + } + }, + "/api/RemoveUser": { + "post": { + "summary": "RemoveUser", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" } } } }, - "description": "Successfully processed retention tag operations" + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } }, - "400": { - "description": "Bad request - missing required parameters", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "string" - } - } + "x-cipp-role": "Identity.User.ReadWrite", + "parameters": [ + { + "name": "ID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "userPrincipalName", + "in": "query", + "required": false, + "schema": { + "type": "string" } } - }, - "403": { - "description": "Forbidden - insufficient permissions", + ], + "requestBody": { + "required": true, "content": { "application/json": { "schema": { - "type": "array", - "items": { - "type": "string" - } + "type": "object", + "properties": { + "ID": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "userPrincipalName": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] } } } } } - } - }, - "/ExecSetMailboxRetentionPolicies": { - "post": { - "description": "Apply a retention policy to one or more mailboxes", - "summary": "Set Mailbox Retention Policies", - "tags": ["POST"], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" + }, + "/api/RemoveUserDefaultTemplate": { + "post": { + "summary": "RemoveUserDefaultTemplate", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } }, - "name": "tenantFilter", - "in": "query" - } - ], - "requestBody": { - "content": { - "application/json": { + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Identity.User.ReadWrite", + "parameters": [ + { + "name": "ID", + "in": "query", + "required": false, "schema": { - "type": "object", - "properties": { - "PolicyName": { - "type": "string", - "description": "Name of the retention policy to apply" - }, - "Mailboxes": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Array of mailbox identities (email addresses or names) to apply the policy to" - } - }, - "required": ["PolicyName", "Mailboxes"] + "type": "string" } } - } - }, - "responses": { - "200": { + ], + "requestBody": { + "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { - "Results": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Array of result messages for each mailbox operation" + "ID": { + "type": "string" } } } } - }, - "description": "Successfully processed mailbox retention policy assignments" - }, - "400": { - "description": "Bad request - missing required parameters", - "content": { - "application/json": { - "schema": { - "type": "string" + } + } + } + }, + "/api/SetAuthMethod": { + "post": { + "summary": "SetAuthMethod", + "tags": [ + "Tenant > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } } } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" } }, - "403": { - "description": "Forbidden - insufficient permissions", + "x-cipp-role": "Tenant.Administration.ReadWrite", + "requestBody": { + "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { - "Results": { - "type": "array", - "items": { - "type": "string" - } + "GroupIds": { + "type": "string" + }, + "Id": { + "type": "string" + }, + "state": { + "type": "string" + }, + "tenantFilter": { + "type": "string" } - } + }, + "required": [ + "tenantFilter" + ] } } } } } - } - }, - "openapi": "3.1.0", - "servers": [ - { - "variables": { - "url": { - "description": "The base URL for the API. Enter your server URL here.", - "default": "CIPPURL.com" - } - }, - "url": "https://{url}/api/", - "description": "CIPP-API" - } - ], - "tags": [ - { - "name": "GET", - "description": "GET methods" }, - { - "name": "POST", - "description": "POST methods" + "/api/listStandardTemplates": { + "get": { + "summary": "listStandardTemplates", + "tags": [ + "Tenant > Standards" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request — missing required field or invalid input" + }, + "401": { + "description": "Unauthorized — invalid or missing bearer token" + }, + "403": { + "description": "Forbidden — caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Standards.Read", + "parameters": [ + { + "name": "id", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } } - ] -} + } +} \ No newline at end of file From 8341f49d863900a36070225eb9c39b0813c91c94 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 31 May 2026 23:15:16 +0200 Subject: [PATCH 084/202] Test --- .../CIPP/MCP/Get-CippMcpSpec.ps1 | 39 +++++ .../CIPP/MCP/Get-CippMcpToolList.ps1 | 149 ++++++++++++++++++ .../CIPP/MCP/Get-CippMcpToolResult.ps1 | 84 ++++++++++ .../CIPP/MCP/Invoke-ExecMcp.ps1 | 74 +++++++++ 4 files changed, 346 insertions(+) create mode 100644 Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/MCP/Get-CippMcpSpec.ps1 create mode 100644 Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/MCP/Get-CippMcpToolList.ps1 create mode 100644 Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/MCP/Get-CippMcpToolResult.ps1 create mode 100644 Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/MCP/Invoke-ExecMcp.ps1 diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/MCP/Get-CippMcpSpec.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/MCP/Get-CippMcpSpec.ps1 new file mode 100644 index 000000000000..341f7eb08f9f --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/MCP/Get-CippMcpSpec.ps1 @@ -0,0 +1,39 @@ +function Get-CippMcpSpec { + <# + .SYNOPSIS + Loads and caches the CIPP OpenAPI specification (openapi.json). + .DESCRIPTION + Returns the parsed OpenAPI document used to project the MCP tool list. The result + is cached per worker runspace; pass -Force to reload (e.g. after the spec is + regenerated). Not an HTTP entrypoint. + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param([switch]$Force) + + if ($script:CippMcpSpec -and -not $Force) { + return $script:CippMcpSpec + } + + $Root = $env:CIPPRootPath + if (-not $Root -or -not (Test-Path (Join-Path $Root 'openapi.json'))) { + # Fallback: walk up from this module until openapi.json is found. + $Root = $PSScriptRoot + while ($Root -and -not (Test-Path (Join-Path $Root 'openapi.json'))) { + $Parent = Split-Path $Root -Parent + if (-not $Parent -or $Parent -eq $Root) { $Root = $null; break } + $Root = $Parent + } + } + + $SpecPath = if ($Root) { Join-Path $Root 'openapi.json' } else { $null } + if (-not $SpecPath -or -not (Test-Path $SpecPath)) { + throw [pscustomobject]@{ code = -32603; message = 'OpenAPI spec (openapi.json) not found; cannot project MCP tools.' } + } + + # -AsHashtable is required: the spec contains objects with case-differing keys + # (e.g. displayName / DisplayName) which a case-insensitive PSCustomObject cannot hold. + $script:CippMcpSpec = [System.IO.File]::ReadAllText($SpecPath) | ConvertFrom-Json -AsHashtable -Depth 100 + return $script:CippMcpSpec +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/MCP/Get-CippMcpToolList.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/MCP/Get-CippMcpToolList.ps1 new file mode 100644 index 000000000000..a102a82e340b --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/MCP/Get-CippMcpToolList.ps1 @@ -0,0 +1,149 @@ +function Get-CippMcpToolList { + <# + .SYNOPSIS + Projects the CIPP OpenAPI spec into the read-only MCP tool list. + .DESCRIPTION + Returns every operation whose x-cipp-role ends in '.Read' (never '.ReadWrite') as an + MCP tool definition: name (the API endpoint), description, inputSchema (JSON Schema + built from the operation's query parameters / request body with $ref inlined), and + read-only annotations. Cached per worker; pass -Force to rebuild. Not an entrypoint. + The spec is consumed as nested hashtables (Get-CippMcpSpec uses -AsHashtable). + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param([switch]$Force) + + if ($script:CippMcpToolListCache -and -not $Force) { + return $script:CippMcpToolListCache + } + + $Spec = Get-CippMcpSpec + $Tools = [System.Collections.Generic.List[object]]::new() + + foreach ($PathEntry in $Spec['paths'].GetEnumerator()) { + $Endpoint = $PathEntry.Key -replace '^/api/', '' + + # Never expose the MCP transport itself as a tool. + if ($Endpoint -eq 'ExecMcp') { continue } + + foreach ($MethodEntry in $PathEntry.Value.GetEnumerator()) { + $Method = [string]$MethodEntry.Key + if ($Method -notin @('get', 'post')) { continue } + + $Op = $MethodEntry.Value + $Role = $Op['x-cipp-role'] + + # Read-only surface only. + if (-not $Role -or $Role -notmatch '\.Read$') { continue } + + # Defensive backstop: never expose an endpoint whose name implies a mutation, + # even if its x-cipp-role is mislabeled '.Read' (e.g. AddTestReport, EditIntunePolicy). + if ($Endpoint -match '^(Add|Set|Remove|Delete|Edit|New|Update|Disable|Enable|Reset|Revoke|Push|Clear|Start|Stop|Rename|Move|Copy)') { continue } + + $Properties = [ordered]@{} + $RequiredList = [System.Collections.Generic.List[string]]::new() + + # Query / path parameters. + foreach ($ParamRaw in @($Op['parameters'])) { + if (-not $ParamRaw) { continue } + $Param = Resolve-CippMcpNode -Node $ParamRaw -Spec $Spec + if ($Param['in'] -notin @('query', 'path')) { continue } + $Schema = if ($Param['schema']) { $Param['schema'] } else { @{ type = 'string' } } + $Properties[[string]$Param['name']] = $Schema + if ($Param['required']) { $RequiredList.Add([string]$Param['name']) } + } + + # Request body (uncommon for reads; included for completeness). + if ($Op['requestBody'] -and $Op['requestBody']['content'] -and $Op['requestBody']['content']['application/json']) { + $BodySchema = Resolve-CippMcpNode -Node $Op['requestBody']['content']['application/json']['schema'] -Spec $Spec + if ($BodySchema -and $BodySchema['properties']) { + foreach ($BodyProp in $BodySchema['properties'].GetEnumerator()) { + $Properties[[string]$BodyProp.Key] = $BodyProp.Value + } + foreach ($Req in @($BodySchema['required'])) { if ($Req) { $RequiredList.Add([string]$Req) } } + } + } + + $InputSchema = [ordered]@{ + type = 'object' + properties = $Properties + } + if ($RequiredList.Count -gt 0) { + $InputSchema['required'] = @($RequiredList | Select-Object -Unique) + } + + $Tools.Add([ordered]@{ + name = $Endpoint + description = Get-CippMcpDescription -Operation $Op + inputSchema = $InputSchema + annotations = [ordered]@{ title = $Endpoint; readOnlyHint = $true } + }) + } + } + + $script:CippMcpToolListCache = $Tools + return $Tools +} + +function Resolve-CippMcpNode { + # Deep-resolves a parsed OpenAPI node (hashtable/array/scalar), inlining any $ref. Internal helper. + param($Node, $Spec, [int]$Depth = 0, [string[]]$Seen = @()) + + if ($null -eq $Node) { return $null } + if ($Depth -gt 15) { return @{ type = 'object' } } + if ($Node -is [string] -or $Node -is [valuetype]) { return $Node } + + if ($Node -is [System.Collections.IDictionary]) { + if ($Node.Contains('$ref')) { + $Ref = [string]$Node['$ref'] + if ($Seen -contains $Ref) { return [ordered]@{ type = 'object'; description = 'recursive reference omitted' } } + $Target = Resolve-CippMcpRef -Ref $Ref -Spec $Spec + return Resolve-CippMcpNode -Node $Target -Spec $Spec -Depth ($Depth + 1) -Seen ($Seen + $Ref) + } + $Out = [ordered]@{} + foreach ($Entry in $Node.GetEnumerator()) { + if ($Entry.Key -eq '$ref') { continue } + $Out[[string]$Entry.Key] = Resolve-CippMcpNode -Node $Entry.Value -Spec $Spec -Depth ($Depth + 1) -Seen $Seen + } + return $Out + } + + if ($Node -is [System.Collections.IEnumerable]) { + return @($Node | ForEach-Object { Resolve-CippMcpNode -Node $_ -Spec $Spec -Depth ($Depth + 1) -Seen $Seen }) + } + + return $Node +} + +function Resolve-CippMcpRef { + # Resolves a JSON pointer like '#/components/parameters/tenantFilter' against the spec. Internal helper. + param([string]$Ref, $Spec) + + $Segments = $Ref.TrimStart('#') -split '/' | Where-Object { $_ -ne '' } + $Node = $Spec + foreach ($Seg in $Segments) { + $Key = $Seg -replace '~1', '/' -replace '~0', '~' + if ($Node -is [System.Collections.IDictionary] -and $Node.Contains($Key)) { + $Node = $Node[$Key] + } else { + return $null + } + } + return $Node +} + +function Get-CippMcpDescription { + # Cleans the operation description (strips leaked PowerShell help) and prefixes the tag. Internal helper. + param($Operation) + + $Desc = [string]$Operation['description'] + $Desc = $Desc -replace '(?s)\s*#>.*$', '' + $Desc = $Desc -replace '(?s)\[CmdletBinding.*$', '' + $Desc = $Desc.Trim() + if ([string]::IsNullOrWhiteSpace($Desc)) { $Desc = [string]$Operation['summary'] } + + $Tag = @($Operation['tags'])[0] + if ($Tag -and $Tag -ne 'Uncategorized') { $Desc = "[$Tag] $Desc" } + return $Desc +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/MCP/Get-CippMcpToolResult.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/MCP/Get-CippMcpToolResult.ps1 new file mode 100644 index 000000000000..073846f871f0 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/MCP/Get-CippMcpToolResult.ps1 @@ -0,0 +1,84 @@ +function Get-CippMcpToolResult { + <# + .SYNOPSIS + Executes a single MCP 'tools/call' by re-dispatching it through the CIPP API router. + .DESCRIPTION + Validates the requested tool against the read-only MCP tool list, then invokes the + corresponding /api endpoint via New-CippCoreRequest using the caller's own principal + headers. This guarantees Test-CIPPAccess (RBAC + tenant scoping + logging) runs for + every tool call exactly as for a normal API request. The synthetic request is tagged + with 'X-CIPP-Origin: mcp' so model-initiated calls are distinguishable in logs. + Returns an MCP tool result object ({ content, isError }). Not an HTTP entrypoint. + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + $Request, + $TriggerMetadata, + [string]$ToolName, + $Arguments + ) + + if ([string]::IsNullOrWhiteSpace($ToolName)) { + throw [pscustomobject]@{ code = -32602; message = 'Invalid params: tool name is required' } + } + + # The tool list is the read-only allowlist; anything not in it cannot be called. + $Tool = Get-CippMcpToolList | Where-Object { $_.name -eq $ToolName } | Select-Object -First 1 + if (-not $Tool) { + throw [pscustomobject]@{ code = -32602; message = "Unknown or unavailable tool: $ToolName" } + } + + # Determine the HTTP method from the spec (defaults to GET for the read surface). + $Spec = Get-CippMcpSpec + $PathItem = $Spec['paths']["/api/$ToolName"] + $Method = if ($PathItem -and $PathItem.Contains('post')) { 'POST' } else { 'GET' } + + # Flatten the MCP arguments object into a parameter hashtable. + $ArgHash = @{} + if ($Arguments) { + foreach ($Prop in $Arguments.PSObject.Properties) { + $ArgHash[$Prop.Name] = $Prop.Value + } + } + + # Clone caller headers (preserves the EasyAuth principal) and tag the origin for auditing. + $Headers = @{} + if ($Request.Headers) { + foreach ($Header in $Request.Headers.PSObject.Properties) { + $Headers[$Header.Name] = $Header.Value + } + } + $Headers['X-CIPP-Origin'] = 'mcp' + + $Query = @{} + $Body = @{} + if ($Method -eq 'GET') { $Query = $ArgHash } else { $Body = $ArgHash } + + $InnerRequest = [pscustomobject]@{ + Params = @{ CIPPEndpoint = $ToolName } + Method = $Method + Headers = $Headers + Query = $Query + Body = $Body + } + + try { + $Response = New-CippCoreRequest -Request $InnerRequest -TriggerMetadata $TriggerMetadata + } catch { + return [ordered]@{ + content = @(@{ type = 'text'; text = "Tool execution failed: $($_.Exception.Message)" }) + isError = $true + } + } + + $ResultBody = $Response.Body + $Text = if ($ResultBody -is [string]) { $ResultBody } else { $ResultBody | ConvertTo-Json -Depth 20 -Compress } + $IsError = $null -ne $Response.StatusCode -and [int]$Response.StatusCode -ge 400 + + return [ordered]@{ + content = @(@{ type = 'text'; text = "$Text" }) + isError = $IsError + } +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/MCP/Invoke-ExecMcp.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/MCP/Invoke-ExecMcp.ps1 new file mode 100644 index 000000000000..19514f9db6fa --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/MCP/Invoke-ExecMcp.ps1 @@ -0,0 +1,74 @@ +function Invoke-ExecMcp { + <# + .SYNOPSIS + Model Context Protocol (MCP) server endpoint for CIPP. + .DESCRIPTION + A Streamable-HTTP MCP server running in JSON response mode (no SSE). It exposes + CIPP's read-only API surface as MCP tools, projected at runtime from openapi.json. + + Every 'tools/call' is re-dispatched through New-CippCoreRequest using the caller's + own principal headers, so the standard RBAC and tenant scoping in Test-CIPPAccess + is enforced for each tool exactly as it would be for a normal API request. This + endpoint's own role (CIPP.Core.Read) is only the floor required to use MCP at all. + .FUNCTIONALITY + Entrypoint + .ROLE + CIPP.Core.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + # v1 supports HTTP POST (JSON response mode) only. SSE/GET streaming is not enabled. + if ($Request.Method -and $Request.Method -ne 'POST') { + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::MethodNotAllowed + Headers = @{ 'Content-Type' = 'application/json'; 'Allow' = 'POST' } + Body = (@{ jsonrpc = '2.0'; id = $null; error = @{ code = -32600; message = 'Only HTTP POST (JSON mode) is supported.' } } | ConvertTo-Json -Compress) + }) + } + + $Rpc = $Request.Body + $RpcId = $Rpc.id + + # JSON-RPC notifications carry no id and receive no response body. + if ($null -eq $RpcId) { + return ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::Accepted }) + } + + try { + if (-not $Rpc.method) { + throw [pscustomobject]@{ code = -32600; message = 'Invalid Request: missing method' } + } + + switch ($Rpc.method) { + 'initialize' { + $Result = [ordered]@{ + protocolVersion = $Rpc.params.protocolVersion ?? '2025-06-18' + capabilities = @{ tools = @{ listChanged = $false } } + serverInfo = [ordered]@{ + name = 'CIPP' + version = $Request.Headers.'X-CIPP-Version' ?? 'unknown' + } + } + } + 'ping' { $Result = @{} } + 'tools/list' { $Result = [ordered]@{ tools = @(Get-CippMcpToolList) } } + 'tools/call' { + $Result = Get-CippMcpToolResult -Request $Request -TriggerMetadata $TriggerMetadata -ToolName $Rpc.params.name -Arguments $Rpc.params.arguments + } + default { throw [pscustomobject]@{ code = -32601; message = "Method not found: $($Rpc.method)" } } + } + + $ResponseBody = [ordered]@{ jsonrpc = '2.0'; id = $RpcId; result = $Result } + } catch { + $Code = $_.TargetObject.code ?? -32603 + $Message = $_.TargetObject.message ?? $_.Exception.Message + $ResponseBody = [ordered]@{ jsonrpc = '2.0'; id = $RpcId; error = [ordered]@{ code = $Code; message = "$Message" } } + } + + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Headers = @{ 'Content-Type' = 'application/json' } + Body = ($ResponseBody | ConvertTo-Json -Depth 30 -Compress) + }) +} From 26a87b279488aa2375d055e1aefbf5c20f33004a Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 1 Jun 2026 00:49:42 +0200 Subject: [PATCH 085/202] add-member force --- .../Public/Authentication/Set-CippApiAuth.ps1 | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Modules/CIPPCore/Public/Authentication/Set-CippApiAuth.ps1 b/Modules/CIPPCore/Public/Authentication/Set-CippApiAuth.ps1 index 1e1dc84a1fb6..74fa1ce71e0b 100644 --- a/Modules/CIPPCore/Public/Authentication/Set-CippApiAuth.ps1 +++ b/Modules/CIPPCore/Public/Authentication/Set-CippApiAuth.ps1 @@ -41,7 +41,7 @@ function Set-CippApiAuth { # The env var has the raw config (identityProviders at top level, no properties wrapper) # Safely navigate/create the full path — any level may be null if (-not $Current.ContainsKey('identityProviders') -or $null -eq $Current.identityProviders) { $Current.identityProviders = @{} } - if (-not $Current.identityProviders.ContainsKey('azureActiveDirectory') -or $null -eq $Current.identityProviders.azureActiveDirectory) { $Current.identityProviders.azureActiveDirectory = @{} } + if (-not $Current.identityProviders.ContainsKey('azureActiveDirectory') -or $null -eq $Current.identityProviders.azureActiveDirectory) { $Current.identityProviders | Add-Member -MemberType NoteProperty -Name 'azureActiveDirectory' -Value @{} -Force } $AAD = $Current.identityProviders.azureActiveDirectory Write-Information "[ApiAuth] AAD keys: $($AAD.Keys -join ', ')" @@ -89,7 +89,7 @@ function Set-CippApiAuth { $PutUri = "$BaseUri/config/authsettingsV2?api-version=2020-06-01" $PutResult = New-CIPPAzRestRequest -Uri $PutUri -Method PUT -Body $PutBody -ContentType 'application/json' -ErrorAction Stop Write-Information "[ApiAuth] PUT result: $($PutResult | ConvertTo-Json -Depth 10 -Compress)" - Write-Information "[ApiAuth] Updated EasyAuth successfully" + Write-Information '[ApiAuth] Updated EasyAuth successfully' } } else { # Full overwrite path (no SSO EasyAuth config to preserve) @@ -106,7 +106,7 @@ function Set-CippApiAuth { if (!$ClientIds) { $ClientIds = @() } if (($ClientIds | Measure-Object).Count -gt 0) { - $AuthSettings.properties.identityProviders.azureActiveDirectory = @{ + $AuthSettings.properties.identityProviders | Add-Member -MemberType NoteProperty -Name 'azureActiveDirectory' -Value @{ enabled = $true registration = @{ clientId = $ClientIds[0] ?? $ClientIds @@ -118,13 +118,14 @@ function Set-CippApiAuth { allowedApplications = @($ClientIds) } } - } + } -Force } else { - $AuthSettings.properties.identityProviders.azureActiveDirectory = @{ + #Replaced with add-member -force + $AuthSettings.properties.identityProviders | Add-Member -MemberType NoteProperty -Name 'azureActiveDirectory' -Value @{ enabled = $false registration = @{} validation = @{} - } + } -Force } $AuthSettings.properties.globalValidation = @{ From 2a2851b57a7a19f2105f2d892cab3c1b94df953d Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 1 Jun 2026 00:58:30 +0200 Subject: [PATCH 086/202] sso auth --- .../Authentication/Set-CIPPSSOEasyAuth.ps1 | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/Modules/CIPPCore/Public/Authentication/Set-CIPPSSOEasyAuth.ps1 b/Modules/CIPPCore/Public/Authentication/Set-CIPPSSOEasyAuth.ps1 index 5ca8abadae37..8ced685da842 100644 --- a/Modules/CIPPCore/Public/Authentication/Set-CIPPSSOEasyAuth.ps1 +++ b/Modules/CIPPCore/Public/Authentication/Set-CIPPSSOEasyAuth.ps1 @@ -95,7 +95,7 @@ function Set-CIPPSSOEasyAuth { # Safely navigate to AAD registration if (-not $Current.ContainsKey('identityProviders') -or $null -eq $Current.identityProviders) { $Current.identityProviders = @{} } - if (-not $Current.identityProviders.ContainsKey('azureActiveDirectory') -or $null -eq $Current.identityProviders.azureActiveDirectory) { $Current.identityProviders.azureActiveDirectory = @{} } + if (-not $Current.identityProviders.ContainsKey('azureActiveDirectory') -or $null -eq $Current.identityProviders.azureActiveDirectory) { $Current.identityProviders | Add-Member -MemberType NoteProperty -Name 'azureActiveDirectory' -Value @{} -Force } $AAD = $Current.identityProviders.azureActiveDirectory if (-not $AAD.ContainsKey('registration') -or $null -eq $AAD.registration) { $AAD.registration = @{} } @@ -131,10 +131,10 @@ function Set-CIPPSSOEasyAuth { # Full overwrite: initial setup — build the entire authsettingsV2 from scratch $AuthConfig = @{ properties = @{ - platform = @{ enabled = $true } - globalValidation = @{ + platform = @{ enabled = $true } + globalValidation = @{ unauthenticatedClientAction = 'RedirectToLoginPage' - redirectToProvider = 'azureactivedirectory' + redirectToProvider = 'azureactivedirectory' excludedPaths = @( '/api/Public*' '/api/setup/health' @@ -144,17 +144,17 @@ function Set-CIPPSSOEasyAuth { azureActiveDirectory = @{ enabled = $true registration = $(if ($ImplicitAuth) { - @{ - clientId = $AppId - openIdIssuer = $IssuerUrl - } - } else { - @{ - clientId = $AppId - clientSecretSettingName = 'AUTH_SECRET' - openIdIssuer = $IssuerUrl - } - }) + @{ + clientId = $AppId + openIdIssuer = $IssuerUrl + } + } else { + @{ + clientId = $AppId + clientSecretSettingName = 'AUTH_SECRET' + openIdIssuer = $IssuerUrl + } + }) validation = @{ allowedAudiences = @("api://$AppId") defaultAuthorizationPolicy = @{ @@ -164,7 +164,7 @@ function Set-CIPPSSOEasyAuth { } } } - login = @{ + login = @{ tokenStore = @{ enabled = $true tokenRefreshExtensionHours = 72 From acf4bf337f4545c5fc5fce2b93c540023c100c91 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 1 Jun 2026 01:05:53 +0200 Subject: [PATCH 087/202] Add or update the Azure App Service build and deployment workflow config --- .github/workflows/dev_cippjta72.yml | 32 +++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/dev_cippjta72.yml diff --git a/.github/workflows/dev_cippjta72.yml b/.github/workflows/dev_cippjta72.yml new file mode 100644 index 000000000000..4f143bb4b2df --- /dev/null +++ b/.github/workflows/dev_cippjta72.yml @@ -0,0 +1,32 @@ +# Docs for the Azure Web Apps Deploy action: https://github.com/azure/functions-action +# More GitHub Actions for Azure: https://github.com/Azure/actions + +name: Build and deploy Powershell project to Azure Function App - cippjta72 + +on: + push: + branches: + - dev + workflow_dispatch: + +env: + AZURE_FUNCTIONAPP_PACKAGE_PATH: '.' # set this to the path to your web app project, defaults to the repository root + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: 'Checkout GitHub Action' + uses: actions/checkout@v4 + + - name: 'Run Azure Functions Action' + uses: Azure/functions-action@v1 + id: fa + with: + app-name: 'cippjta72' + slot-name: 'Production' + package: ${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }} + publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_1EBE9D73F9EC4528BA666FC934055536 }} + sku: 'flexconsumption' + \ No newline at end of file From 72f7882fe5c042f4e05353e8fe27a10c6d4a5456 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 1 Jun 2026 01:16:02 +0200 Subject: [PATCH 088/202] fixes another add member --- .../CIPPCore/Public/Authentication/Set-CippApiAuth.ps1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Modules/CIPPCore/Public/Authentication/Set-CippApiAuth.ps1 b/Modules/CIPPCore/Public/Authentication/Set-CippApiAuth.ps1 index 74fa1ce71e0b..5b2c57c2c853 100644 --- a/Modules/CIPPCore/Public/Authentication/Set-CippApiAuth.ps1 +++ b/Modules/CIPPCore/Public/Authentication/Set-CippApiAuth.ps1 @@ -128,15 +128,15 @@ function Set-CippApiAuth { } -Force } - $AuthSettings.properties.globalValidation = @{ + $AuthSettings.properties | Add-Member -MemberType NoteProperty -Name 'globalValidation' -Value @{ unauthenticatedClientAction = 'Return401' - } - $AuthSettings.properties.login = @{ + } -Force + $AuthSettings.properties | Add-Member -MemberType NoteProperty -Name 'login' -Value @{ tokenStore = @{ enabled = $true tokenRefreshExtensionHours = 72 } - } + } -Force if ($PSCmdlet.ShouldProcess('Update auth settings')) { $putUri = "$BaseUri/config/authsettingsV2?api-version=2020-06-01" From 54a3a0821de35a718357f26c98be908f3bbf1a7b Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Mon, 1 Jun 2026 12:39:14 +0800 Subject: [PATCH 089/202] api auth save and get changes --- .../Public/Authentication/Get-CippApiAuth.ps1 | 89 ++++++++++++------- .../Public/Authentication/Set-CippApiAuth.ps1 | 42 +++++---- 2 files changed, 80 insertions(+), 51 deletions(-) diff --git a/Modules/CIPPCore/Public/Authentication/Get-CippApiAuth.ps1 b/Modules/CIPPCore/Public/Authentication/Get-CippApiAuth.ps1 index faea057c4fa1..a943bc149716 100644 --- a/Modules/CIPPCore/Public/Authentication/Get-CippApiAuth.ps1 +++ b/Modules/CIPPCore/Public/Authentication/Get-CippApiAuth.ps1 @@ -4,53 +4,74 @@ function Get-CippApiAuth { [string]$FunctionAppName ) - $AuthSettings = $null + if ($env:CIPPNG) { + $AuthSettings = $null - # When the auth config is available as an env var, use it directly (no ARM call needed) - if ($env:CIPPNG -and $env:WEBSITE_AUTH_V2_CONFIG_JSON) { - $AuthSettings = $env:WEBSITE_AUTH_V2_CONFIG_JSON | ConvertFrom-Json -ErrorAction SilentlyContinue - } + # When the auth config is available as an env var, use it directly (no ARM call needed) + if ($env:WEBSITE_AUTH_V2_CONFIG_JSON) { + $AuthSettings = $env:WEBSITE_AUTH_V2_CONFIG_JSON | ConvertFrom-Json -ErrorAction SilentlyContinue + } + + # Fall back to reading via ARM REST + if (-not $AuthSettings) { + $SubscriptionId = Get-CIPPAzFunctionAppSubId + try { + $uri = "https://management.azure.com/subscriptions/$SubscriptionId/resourceGroups/$RGName/providers/Microsoft.Web/sites/$($FunctionAppName)/config/authsettingsV2/list?api-version=2020-06-01" + $response = New-CIPPAzRestRequest -Uri $uri -Method POST -ErrorAction Stop + $AuthSettings = $response.properties + } catch { + Write-Warning "Failed to get auth settings via REST: $($_.Exception.Message)" + } + } + + if ($AuthSettings) { + $AAD = $AuthSettings.identityProviders.azureActiveDirectory + $Issuer = $AAD.registration.openIdIssuer ?? '' + $AllowedApps = @($AAD.validation.defaultAuthorizationPolicy.allowedApplications) + + # When SSO EasyAuth is in use, filter out its clientId — the frontend only tracks API clients + $SSOClientId = $AAD.registration.clientId + if ($SSOClientId) { + $AllowedApps = @($AllowedApps | Where-Object { $_ -ne $SSOClientId }) + } - # Fall back to reading via ARM REST - if (-not $AuthSettings) { + $ExtractedTenantId = $Issuer -replace 'https://sts.windows.net/', '' -replace 'https://login.microsoftonline.com/', '' -replace '/v2.0', '' + $TenantId = if ($ExtractedTenantId -eq 'common') { $env:TenantID } else { $ExtractedTenantId } + + [PSCustomObject]@{ + ApiUrl = "https://$($env:WEBSITE_HOSTNAME)" + TenantID = $TenantId + ClientIDs = $AllowedApps + Enabled = $AAD.enabled + } + } else { + throw 'No auth settings found' + } + } else { $SubscriptionId = Get-CIPPAzFunctionAppSubId + try { + # Get auth settings via REST $uri = "https://management.azure.com/subscriptions/$SubscriptionId/resourceGroups/$RGName/providers/Microsoft.Web/sites/$($FunctionAppName)/config/authsettingsV2/list?api-version=2020-06-01" $response = New-CIPPAzRestRequest -Uri $uri -Method POST -ErrorAction Stop $AuthSettings = $response.properties } catch { Write-Warning "Failed to get auth settings via REST: $($_.Exception.Message)" } - } - # Fallback to env var if ARM failed - if (-not $AuthSettings -and $env:WEBSITE_AUTH_V2_CONFIG_JSON) { - $AuthSettings = $env:WEBSITE_AUTH_V2_CONFIG_JSON | ConvertFrom-Json -ErrorAction SilentlyContinue - } - - if ($AuthSettings) { - $AAD = $AuthSettings.identityProviders.azureActiveDirectory - $Issuer = $AAD.registration.openIdIssuer ?? '' - $AllowedApps = @($AAD.validation.defaultAuthorizationPolicy.allowedApplications) - - # When SSO EasyAuth is in use, filter out its clientId — the frontend only tracks API clients - if ($env:CIPPNG) { - $SSOClientId = $AAD.registration.clientId - if ($SSOClientId) { - $AllowedApps = @($AllowedApps | Where-Object { $_ -ne $SSOClientId }) - } + if (!$AuthSettings -and $env:WEBSITE_AUTH_V2_CONFIG_JSON) { + $AuthSettings = $env:WEBSITE_AUTH_V2_CONFIG_JSON | ConvertFrom-Json -ErrorAction SilentlyContinue } - $ExtractedTenantId = $Issuer -replace 'https://sts.windows.net/', '' -replace 'https://login.microsoftonline.com/', '' -replace '/v2.0', '' - $TenantId = if ($ExtractedTenantId -eq 'common') { $env:TenantID } else { $ExtractedTenantId } - - [PSCustomObject]@{ - ApiUrl = "https://$($env:WEBSITE_HOSTNAME)" - TenantID = $TenantId - ClientIDs = $AllowedApps - Enabled = $AAD.enabled + if ($AuthSettings) { + [PSCustomObject]@{ + ApiUrl = "https://$($env:WEBSITE_HOSTNAME)" + TenantID = $AuthSettings.identityProviders.azureActiveDirectory.registration.openIdIssuer -replace 'https://sts.windows.net/', '' -replace '/v2.0', '' + ClientIDs = $AuthSettings.identityProviders.azureActiveDirectory.validation.defaultAuthorizationPolicy.allowedApplications + Enabled = $AuthSettings.identityProviders.azureActiveDirectory.enabled + } + } else { + throw 'No auth settings found' } - } else { - throw 'No auth settings found' } } diff --git a/Modules/CIPPCore/Public/Authentication/Set-CippApiAuth.ps1 b/Modules/CIPPCore/Public/Authentication/Set-CippApiAuth.ps1 index 5b2c57c2c853..f9e421b24a31 100644 --- a/Modules/CIPPCore/Public/Authentication/Set-CippApiAuth.ps1 +++ b/Modules/CIPPCore/Public/Authentication/Set-CippApiAuth.ps1 @@ -92,21 +92,30 @@ function Set-CippApiAuth { Write-Information '[ApiAuth] Updated EasyAuth successfully' } } else { - # Full overwrite path (no SSO EasyAuth config to preserve) + # Resolve subscription ID via helper (managed identity environment assumed for ARM). $SubscriptionId = Get-CIPPAzFunctionAppSubId - $BaseUri = "https://management.azure.com/subscriptions/$SubscriptionId/resourceGroups/$RGName/providers/Microsoft.Web/sites/$FunctionAppName" - $getUri = "$BaseUri/config/authsettingsV2/list?api-version=2020-06-01" - $AuthSettings = New-CIPPAzRestRequest -Uri $getUri -Method POST + # Get auth settings via ARM REST (managed identity) + $getUri = "https://management.azure.com/subscriptions/$SubscriptionId/resourceGroups/$RGName/providers/Microsoft.Web/sites/$($FunctionAppName)/config/authsettingsV2/list?api-version=2020-06-01" + $resp = New-CIPPAzRestRequest -Uri $getUri -Method 'GET' + $AuthSettings = $resp | Select-Object -ExpandProperty Content -ErrorAction SilentlyContinue + if ($AuthSettings -is [string]) { $AuthSettings = $AuthSettings | ConvertFrom-Json } + else { $AuthSettings = $resp } Write-Information "AuthSettings: $($AuthSettings | ConvertTo-Json -Depth 10)" - $AllowedAudiences = foreach ($ClientId in $ClientIds) { "api://$ClientId" } + # Set allowed audiences + $AllowedAudiences = foreach ($ClientId in $ClientIds) { + "api://$ClientId" + } + if (!$AllowedAudiences) { $AllowedAudiences = @() } if (!$ClientIds) { $ClientIds = @() } + # Set auth settings + if (($ClientIds | Measure-Object).Count -gt 0) { - $AuthSettings.properties.identityProviders | Add-Member -MemberType NoteProperty -Name 'azureActiveDirectory' -Value @{ + $AuthSettings.properties.identityProviders.azureActiveDirectory = @{ enabled = $true registration = @{ clientId = $ClientIds[0] ?? $ClientIds @@ -118,30 +127,29 @@ function Set-CippApiAuth { allowedApplications = @($ClientIds) } } - } -Force + } } else { - #Replaced with add-member -force - $AuthSettings.properties.identityProviders | Add-Member -MemberType NoteProperty -Name 'azureActiveDirectory' -Value @{ + $AuthSettings.properties.identityProviders.azureActiveDirectory = @{ enabled = $false registration = @{} validation = @{} - } -Force + } } - $AuthSettings.properties | Add-Member -MemberType NoteProperty -Name 'globalValidation' -Value @{ + $AuthSettings.properties.globalValidation = @{ unauthenticatedClientAction = 'Return401' - } -Force - $AuthSettings.properties | Add-Member -MemberType NoteProperty -Name 'login' -Value @{ + } + $AuthSettings.properties.login = @{ tokenStore = @{ enabled = $true tokenRefreshExtensionHours = 72 } - } -Force + } if ($PSCmdlet.ShouldProcess('Update auth settings')) { - $putUri = "$BaseUri/config/authsettingsV2?api-version=2020-06-01" - $Body = $AuthSettings | ConvertTo-Json -Depth 20 - $null = New-CIPPAzRestRequest -Uri $putUri -Method PUT -Body $Body -ContentType 'application/json' + # Update auth settings via ARM REST + $putUri = "https://management.azure.com/subscriptions/$SubscriptionId/resourceGroups/$RGName/providers/Microsoft.Web/sites/$($FunctionAppName)/config/authsettingsV2?api-version=2020-06-01" + $null = New-CIPPAzRestRequest -Uri $putUri -Method 'PUT' -Body $AuthSettings -ContentType 'application/json' } if ($PSCmdlet.ShouldProcess('Update allowed tenants')) { From c16557ff6587406decf70f6d47d7fe3014da473b Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Mon, 1 Jun 2026 22:26:01 +0800 Subject: [PATCH 090/202] Guarding for cache collection items --- .../DBCache/Set-CIPPDBCacheOneDriveUsage.ps1 | 16 +++++++++++++--- .../Set-CIPPDBCacheSharePointSiteUsage.ps1 | 1 + 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheOneDriveUsage.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheOneDriveUsage.ps1 index 2000025ebd5d..d0f3cc2c58f1 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheOneDriveUsage.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheOneDriveUsage.ps1 @@ -34,11 +34,21 @@ function Set-CIPPDBCacheOneDriveUsage { $Result = New-GraphBulkRequest -tenantid $TenantFilter -Requests @($BulkRequests) -asapp $true $Sites = @(($Result | Where-Object { $_.id -eq 'listAllSites' }).body.value) - $UsageBase64 = ($Result | Where-Object { $_.id -eq 'usage' }).body - $UsageJson = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($UsageBase64)) - $OneDriveUsage = @(($UsageJson | ConvertFrom-Json).value) + + $UsageResponse = $Result | Where-Object { $_.id -eq 'usage' } + if ($UsageResponse.status -and $UsageResponse.status -ne 200) { + throw ($UsageResponse.body.error.message ?? "Usage report request failed with status $($UsageResponse.status)") + } + $UsageBody = $UsageResponse.body + if ($UsageBody -is [string]) { + $UsageJson = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($UsageBody)) + $OneDriveUsage = @(($UsageJson | ConvertFrom-Json).value) + } else { + $OneDriveUsage = @($UsageBody.value) + } foreach ($UsageRow in $OneDriveUsage) { + if ($null -eq $UsageRow) { continue } $UsageRow | Add-Member -NotePropertyName 'id' -NotePropertyValue $UsageRow.siteId -Force $UsageRow | Add-Member -NotePropertyName 'userPrincipalName' -NotePropertyValue $UsageRow.ownerPrincipalName -Force } diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheSharePointSiteUsage.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheSharePointSiteUsage.ps1 index 561a01721236..71f072a4eb74 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheSharePointSiteUsage.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheSharePointSiteUsage.ps1 @@ -51,6 +51,7 @@ function Set-CIPPDBCacheSharePointSiteUsage { # Ensure a stable row key for usage rows. foreach ($UsageRow in $UsageRows) { + if ($null -eq $UsageRow) { continue } $UsageRow | Add-Member -NotePropertyName 'id' -NotePropertyValue $UsageRow.siteId -Force } From 4b4a2d1d7e97aa30525e84a11ef3fc8c89d9dae9 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Mon, 1 Jun 2026 23:01:23 +0800 Subject: [PATCH 091/202] Update Set-CIPPDBCacheSharePointSiteUsage.ps1 --- .../Public/DBCache/Set-CIPPDBCacheSharePointSiteUsage.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheSharePointSiteUsage.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheSharePointSiteUsage.ps1 index 71f072a4eb74..8b7bb765b71b 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheSharePointSiteUsage.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheSharePointSiteUsage.ps1 @@ -100,4 +100,4 @@ function Set-CIPPDBCacheSharePointSiteUsage { } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache SharePoint site usage: $($_.Exception.Message)" -sev Error -LogData (Get-CippException -Exception $_) } -} \ No newline at end of file +} From f3393cb9b163ff916b8882dbaaaaf981d3ae80c8 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 2 Jun 2026 00:09:02 +0800 Subject: [PATCH 092/202] Update Invoke-ExecUniversalSearchV2.ps1 --- .../Invoke-ExecUniversalSearchV2.ps1 | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ExecUniversalSearchV2.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ExecUniversalSearchV2.ps1 index 8c3b9d954fb1..c133159efb85 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ExecUniversalSearchV2.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ExecUniversalSearchV2.ps1 @@ -31,6 +31,60 @@ function Invoke-ExecUniversalSearchV2 { 'Applications' { $Results = Search-CIPPDbData -SearchTerms $SearchTerms -Types 'Apps', 'ServicePrincipals' -Limit $Limit -Properties 'id', 'appId', 'displayName', 'publisherName', 'appOwnerOrganizationId' -TenantFilter $TenantFilter } + 'Licenses' { + # SKU lookup is universal — always search across all tenants regardless of caller scope. + # No Properties filter so service plan names / friendly names embedded in the JSON + # still pass the secondary verification pass. + $Raw = Search-CIPPDbData -SearchTerms $SearchTerms -Types 'LicenseOverview' -TenantFilter 'allTenants' + + $BySku = [ordered]@{} + foreach ($Row in $Raw) { + $Data = $Row.Data + if (-not $Data -or [string]::IsNullOrWhiteSpace($Data.skuId)) { continue } + $Key = ([string]$Data.skuId).ToLowerInvariant() + + if (-not $BySku.Contains($Key)) { + $BySku[$Key] = [PSCustomObject]@{ + skuId = [string]$Data.skuId + skuPartNumber = [string]$Data.skuPartNumber + displayName = [string]$Data.License + servicePlans = @($Data.ServicePlans) + tenantCount = 0 + totalAssigned = 0 + totalAvailable = 0 + tenants = [System.Collections.Generic.List[object]]::new() + } + } + + $Entry = $BySku[$Key] + if ([string]::IsNullOrWhiteSpace($Entry.skuPartNumber) -and $Data.skuPartNumber) { $Entry.skuPartNumber = [string]$Data.skuPartNumber } + if ([string]::IsNullOrWhiteSpace($Entry.displayName) -and $Data.License) { $Entry.displayName = [string]$Data.License } + if ((-not $Entry.servicePlans -or $Entry.servicePlans.Count -eq 0) -and $Data.ServicePlans) { $Entry.servicePlans = @($Data.ServicePlans) } + + $Entry.tenantCount++ + $Used = 0; [int]::TryParse([string]$Data.CountUsed, [ref]$Used) | Out-Null + $Total = 0; [int]::TryParse([string]$Data.TotalLicenses, [ref]$Total) | Out-Null + $Entry.totalAssigned += $Used + $Entry.totalAvailable += $Total + $Entry.tenants.Add([PSCustomObject]@{ + tenant = [string]$Row.Tenant + used = $Used + total = $Total + }) + } + + $Aggregated = $BySku.Values | Sort-Object -Property tenantCount -Descending | Select-Object -First $Limit + + # Shape into the same envelope as other types so the frontend can use match.Data + $Results = foreach ($Item in $Aggregated) { + [PSCustomObject]@{ + Tenant = '' + Type = 'Licenses' + RowKey = "Licenses-$($Item.skuId)" + Data = $Item + } + } + } default { $Results = Search-CIPPDbData -SearchTerms $SearchTerms -Types 'Users' -Limit $Limit -Properties 'id', 'userPrincipalName', 'displayName' -TenantFilter $TenantFilter } From 9d8f1a0dafc1dff686e1501e0bdf9e493b3dea3b Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 2 Jun 2026 00:12:25 +0800 Subject: [PATCH 093/202] correct incorrect pathing --- Modules/CIPPCore/Public/Add-CIPPW32ScriptApplication.ps1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Modules/CIPPCore/Public/Add-CIPPW32ScriptApplication.ps1 b/Modules/CIPPCore/Public/Add-CIPPW32ScriptApplication.ps1 index dc9298da4356..77518b24d11d 100644 --- a/Modules/CIPPCore/Public/Add-CIPPW32ScriptApplication.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPW32ScriptApplication.ps1 @@ -45,15 +45,15 @@ function Add-CIPPW32ScriptApplication { ) # Get the standard Chocolatey package location (relative to function app root) - $IntuneWinFile = 'AddChocoApp\IntunePackage.intunewin' - $ChocoXmlFile = 'AddChocoApp\Choco.App.xml' + $IntuneWinFile = Join-Path $env:CIPPRootPath 'AddChocoApp\IntunePackage.intunewin' + $ChocoXmlFile = Join-Path $env:CIPPRootPath 'AddChocoApp\Choco.App.xml' if (-not (Test-Path $IntuneWinFile)) { - throw "Chocolatey IntunePackage.intunewin not found at: $IntuneWinFile (Current directory: $PWD)" + throw "Chocolatey IntunePackage.intunewin not found at: $IntuneWinFile (CIPPRootPath: $env:CIPPRootPath)" } if (-not (Test-Path $ChocoXmlFile)) { - throw "Choco.App.xml not found at: $ChocoXmlFile (Current directory: $PWD)" + throw "Choco.App.xml not found at: $ChocoXmlFile (CIPPRootPath: $env:CIPPRootPath)" } # Parse the Choco XML to get encryption info. We need a wrapper around the application and this is a tiny intune file, perfect for our purpose. From a40683c560c66ebcd4164515fda9bcf68715bc37 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 1 Jun 2026 13:51:15 -0400 Subject: [PATCH 094/202] fix: access issues related to undefined roles cleanup dead code C-002 --- .../Public/Authentication/Test-CIPPAccess.ps1 | 36 +++---------------- 1 file changed, 4 insertions(+), 32 deletions(-) diff --git a/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 b/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 index af0d8bb74bf6..0dfd4ba6573b 100644 --- a/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 +++ b/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 @@ -320,7 +320,6 @@ function Test-CIPPAccess { $PermissionsFound = $true } catch { Write-Information $_.Exception.Message - continue } } $swRolePerms.Stop() @@ -477,38 +476,12 @@ function Test-CIPPAccess { } else { # No permissions found for any roles if ($TenantList.IsPresent) { - return @('AllTenants') - } - return $true - if ($APIAllowed) { - $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter.value ?? $Request.Body.tenantFilter ?? $Request.Query.tenantId ?? $Request.Body.tenantId.value ?? $Request.Body.tenantId ?? $env:TenantID - # Check tenant level access - if (($Role.BlockedTenants | Measure-Object).Count -eq 0 -and $Role.AllowedTenants -contains 'AllTenants') { - $TenantAllowed = $true - } elseif ($TenantFilter -eq 'AllTenants') { - $TenantAllowed = $false - } else { - $Tenant = ($Tenants | Where-Object { $TenantFilter -eq $_.customerId -or $TenantFilter -eq $_.defaultDomainName }).customerId - - if ($Role.AllowedTenants -contains 'AllTenants') { - $AllowedTenants = $Tenants.customerId - } else { - $AllowedTenants = $Role.AllowedTenants - } - if ($Tenant) { - $TenantAllowed = $AllowedTenants -contains $Tenant -and $Role.BlockedTenants -notcontains $Tenant - if (!$TenantAllowed) { continue } - break - } else { - $TenantAllowed = $true - break - } - } + return @() } + throw 'Access to this CIPP API endpoint is not allowed, the user does not have the required permission' } if (!$TenantAllowed -and $Functionality -notmatch 'AnyTenant') { - if (!$APIAllowed) { throw "Access to this CIPP API endpoint is not allowed, you do not have the required permission: $APIRole" } @@ -519,14 +492,13 @@ function Test-CIPPAccess { } else { return $true } - } } else { # No permissions found for any roles if ($TenantList.IsPresent) { - return @('AllTenants') + return @() } - return $true + throw 'Access to this CIPP API endpoint is not allowed, the user does not have the required permission' } $swUserBranch.Stop() $AccessTimings['UserBranch'] = $swUserBranch.Elapsed.TotalMilliseconds From 3c19e6299a87afa75b05726e2609e9b6da2734d8 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 1 Jun 2026 13:59:15 -0400 Subject: [PATCH 095/202] fix: ip restriction logic H-001 --- Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 b/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 index 0dfd4ba6573b..3e9832be532c 100644 --- a/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 +++ b/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 @@ -160,7 +160,7 @@ function Test-CIPPAccess { } } - if (-not $IPAllowed -and -not $Request.Params.CIPPEndpoint -eq 'me') { + if ((-not $IPAllowed) -and ($Request.Params.CIPPEndpoint -ne 'me')) { throw "Access to this CIPP API endpoint is not allowed, your IP address ($IPAddress) is not in the allowed range for your role(s)" } } else { From 648127f731f598547988e5dd3c581ae1eef225e1 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 1 Jun 2026 14:01:03 -0400 Subject: [PATCH 096/202] fix: role for EditIntunePolicy H-004 --- .../HTTP Functions/Endpoint/MEM/Invoke-EditIntunePolicy.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-EditIntunePolicy.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-EditIntunePolicy.ps1 index 98da51021648..a7844764f852 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-EditIntunePolicy.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-EditIntunePolicy.ps1 @@ -1,9 +1,9 @@ -Function Invoke-EditIntunePolicy { +function Invoke-EditIntunePolicy { <# .FUNCTIONALITY Entrypoint .ROLE - Endpoint.MEM.Read + Endpoint.MEM.ReadWrite #> [CmdletBinding()] param($Request, $TriggerMetadata) @@ -23,7 +23,7 @@ Function Invoke-EditIntunePolicy { # Only add displayName if it's provided if ($DisplayName) { - $properties["displayName"] = $DisplayName + $properties['displayName'] = $DisplayName } # Update the policy From 679c390558470782a3b6badc7942484049840817 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 1 Jun 2026 14:13:52 -0400 Subject: [PATCH 097/202] fix: validate token exchange url is microsoft C-008 --- .../CIPP/Setup/Invoke-ExecTokenExchange.ps1 | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecTokenExchange.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecTokenExchange.ps1 index 516ecdc914af..5621576362f6 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecTokenExchange.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecTokenExchange.ps1 @@ -27,6 +27,15 @@ function Invoke-ExecTokenExchange { throw 'Missing required parameters: tokenRequest or tokenUrl' } + $ParsedTokenUri = $null + $IsValidTokenUri = [System.Uri]::TryCreate($TokenUrl, [System.UriKind]::Absolute, [ref]$ParsedTokenUri) + if (-not $IsValidTokenUri -or + -not $ParsedTokenUri.Scheme.Equals('https', [System.StringComparison]::OrdinalIgnoreCase) -or + -not $ParsedTokenUri.Host.Equals('login.microsoftonline.com', [System.StringComparison]::OrdinalIgnoreCase)) { + Write-LogMessage -API $APIName -message "Blocked token request to non-Microsoft login host: $TokenUrl" -Sev 'Warning' + throw 'Invalid tokenUrl. Only https://login.microsoftonline.com is allowed.' + } + Write-LogMessage -API $APIName -message "Making token request to $TokenUrl" -Sev 'Info' # Make sure we get the latest authentication From 139b0c606ac3d6ed23af64001ab25bd4d70c0806 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 1 Jun 2026 14:30:43 -0400 Subject: [PATCH 098/202] fix: sanitize more odata paths for tables C-006 --- .../Scheduler/Invoke-ListScheduledItemDetails.ps1 | 6 ++++-- .../CIPP/Settings/Invoke-ExecWebhookSubscriptions.ps1 | 11 ++++++++--- .../Administration/Invoke-ListMailboxRules.ps1 | 3 +++ .../HTTP Functions/Invoke-ListKnownIPDb.ps1 | 11 +++++++---- .../Tenant/GDAP/Invoke-ListGDAPInvite.ps1 | 5 +++-- 5 files changed, 25 insertions(+), 11 deletions(-) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-ListScheduledItemDetails.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-ListScheduledItemDetails.ps1 index 57ec66bd8733..c3608506733f 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-ListScheduledItemDetails.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-ListScheduledItemDetails.ps1 @@ -21,9 +21,11 @@ function Invoke-ListScheduledItemDetails { return } + $SafeRowKey = ConvertTo-CIPPODataFilterValue -Value $RowKey -Type String + # Retrieve the task information $TaskTable = Get-CIPPTable -TableName 'ScheduledTasks' - $Task = Get-CIPPAzDataTableEntity @TaskTable -Filter "RowKey eq '$RowKey' and PartitionKey eq 'ScheduledTask'" | Select-Object RowKey, Name, TaskState, Command, Parameters, Recurrence, ExecutedTime, ScheduledTime, PostExecution, Tenant, TenantGroup, Hidden, Results, Timestamp, Trigger + $Task = Get-CIPPAzDataTableEntity @TaskTable -Filter "RowKey eq '$SafeRowKey' and PartitionKey eq 'ScheduledTask'" | Select-Object RowKey, Name, TaskState, Command, Parameters, Recurrence, ExecutedTime, ScheduledTime, PostExecution, Tenant, TenantGroup, Hidden, Results, Timestamp, Trigger if (-not $Task) { return ([HttpResponseContext]@{ @@ -95,7 +97,7 @@ function Invoke-ListScheduledItemDetails { # Get the results if available $ResultsTable = Get-CIPPTable -TableName 'ScheduledTaskResults' - $ResultsFilter = "PartitionKey eq '$RowKey'" + $ResultsFilter = "PartitionKey eq '$SafeRowKey'" $Results = Get-CIPPAzDataTableEntity @ResultsTable -Filter $ResultsFilter diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecWebhookSubscriptions.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecWebhookSubscriptions.ps1 index 9bdea4f141dd..baa30069dc30 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecWebhookSubscriptions.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecWebhookSubscriptions.ps1 @@ -9,9 +9,14 @@ function Invoke-ExecWebhookSubscriptions { param($Request, $TriggerMetadata) $Table = Get-CIPPTable -TableName webhookTable + $WebhookId = $Request.Query.WebhookID + $SafeWebhookId = if (![string]::IsNullOrEmpty($WebhookId)) { + ConvertTo-CIPPODataFilterValue -Value $WebhookId -Type String + } + switch ($Request.Query.Action) { 'Delete' { - $Webhook = Get-AzDataTableEntity @Table -Filter "RowKey eq '$($Request.Query.WebhookID)'" -Property PartitionKey, RowKey + $Webhook = Get-AzDataTableEntity @Table -Filter "RowKey eq '$SafeWebhookId'" -Property PartitionKey, RowKey if ($Webhook) { Remove-CIPPGraphSubscription -TenantFilter $Webhook.PartitionKey -CIPPID $Webhook.RowKey Remove-AzDataTableEntity -Force @Table -Entity $Webhook @@ -27,7 +32,7 @@ function Invoke-ExecWebhookSubscriptions { } } 'Unsubscribe' { - $Webhook = Get-AzDataTableEntity @Table -Filter "RowKey eq '$($Request.Query.WebhookID)'" -Property PartitionKey, RowKey + $Webhook = Get-AzDataTableEntity @Table -Filter "RowKey eq '$SafeWebhookId'" -Property PartitionKey, RowKey if ($Webhook) { $Unsubscribe = @{ TenantFilter = $Webhook.PartitionKey @@ -82,7 +87,7 @@ function Invoke-ExecWebhookSubscriptions { } 'Resubscribe' { Write-Host "Resubscribing to $($Request.Query.WebhookID)" - $Row = Get-AzDataTableEntity @Table -Filter "RowKey eq '$($Request.Query.WebhookID)'" + $Row = Get-AzDataTableEntity @Table -Filter "RowKey eq '$SafeWebhookId'" if ($Row) { $NewSubParams = @{ TenantFilter = $Row.PartitionKey diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxRules.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxRules.ps1 index 40af840aaa4d..7dc29cbea73c 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxRules.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxRules.ps1 @@ -9,6 +9,9 @@ function Invoke-ListMailboxRules { param($Request, $TriggerMetadata) # Interact with query parameters or the body of the request. $TenantFilter = $Request.Query.tenantFilter + if (-not [string]::IsNullOrEmpty($TenantFilter) -and $TenantFilter -ne 'AllTenants') { + $TenantFilter = ConvertTo-CIPPODataFilterValue -Value $TenantFilter -Type String + } $UseReportDB = $Request.Query.UseReportDB try { diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListKnownIPDb.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListKnownIPDb.ps1 index 97b8b70c5b21..5c7f7102130b 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListKnownIPDb.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListKnownIPDb.ps1 @@ -1,4 +1,4 @@ -Function Invoke-ListKnownIPDb { +function Invoke-ListKnownIPDb { <# .FUNCTIONALITY Entrypoint @@ -9,6 +9,9 @@ Function Invoke-ListKnownIPDb { param($Request, $TriggerMetadata) # Interact with query parameters or the body of the request. $TenantFilter = $Request.Query.tenantFilter + if (-not [string]::IsNullOrEmpty($TenantFilter)) { + $TenantFilter = ConvertTo-CIPPODataFilterValue -Value $TenantFilter -Type String + } $Table = Get-CIPPTable -TableName 'knownlocationdbv2' @@ -16,8 +19,8 @@ Function Invoke-ListKnownIPDb { $KnownIPDb = Get-CIPPAzDataTableEntity @Table -Filter $Filter return [HttpResponseContext]@{ - StatusCode = [HttpStatusCode]::OK - Body = @($KnownIPDb) - } + StatusCode = [HttpStatusCode]::OK + Body = @($KnownIPDb) + } } diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPInvite.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPInvite.ps1 index 8dbb1d3e2e2b..e9dd7530895d 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPInvite.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPInvite.ps1 @@ -1,4 +1,4 @@ -Function Invoke-ListGDAPInvite { +function Invoke-ListGDAPInvite { <# .FUNCTIONALITY Entrypoint,AnyTenant @@ -12,7 +12,8 @@ Function Invoke-ListGDAPInvite { $Table = Get-CIPPTable -TableName 'GDAPInvites' if (![string]::IsNullOrEmpty($RelationshipId)) { - $Invite = Get-CIPPAzDataTableEntity @Table -Filter "RowKey eq '$($RelationshipId)'" + $SafeRelationshipId = ConvertTo-CIPPODataFilterValue -Value $RelationshipId -Type String + $Invite = Get-CIPPAzDataTableEntity @Table -Filter "RowKey eq '$SafeRelationshipId'" } else { $Invite = Get-CIPPAzDataTableEntity @Table | ForEach-Object { $_.RoleMappings = @(try { $_.RoleMappings | ConvertFrom-Json } catch { $_.RoleMappings }) From a9797cd95bb25a536e1172b3e2595c6fc8c592ea Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 1 Jun 2026 14:41:56 -0400 Subject: [PATCH 099/202] chore: cleanup redundant tenant check in listexorequest access checks are performed at the new-cippcorerequest level H-003 --- .../Tools/Invoke-ListExoRequest.ps1 | 121 ++++++++---------- 1 file changed, 56 insertions(+), 65 deletions(-) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Tools/Invoke-ListExoRequest.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Tools/Invoke-ListExoRequest.ps1 index 9162632915dd..852efdf0fde2 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Tools/Invoke-ListExoRequest.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Tools/Invoke-ListExoRequest.ps1 @@ -16,82 +16,73 @@ function Invoke-ListExoRequest { $cmdParams = if ($Request.Body.cmdParams) { $Request.Body.cmdParams } else { [PSCustomObject]@{} } $Verb = ($Cmdlet -split '-')[0] - $AllowedTenants = Test-CIPPAccess -Request $Request -TenantList - $TenantFilter = $Request.Body.TenantFilter - $Tenants = Get-Tenants -IncludeErrors - $Tenant = $Tenants | Where-Object { $_.defaultDomainName -eq $TenantFilter -or $_.customerId -eq $TenantFilter } - if ($Tenant.customerId -in $AllowedTenants -or $AllowedTenants -eq 'AllTenants') { - if ($Request.Body.AvailableCmdlets) { - $ExoRequest = @{ - TenantID = $TenantFilter - AvailableCmdlets = $true - } - if ($Request.Body.AsApp -eq $true) { - $ExoRequest.AsApp = $true - } - if ($Request.Body.Compliance -eq $true) { - $ExoRequest.Compliance = $true + if ($Request.Body.AvailableCmdlets) { + $ExoRequest = @{ + TenantID = $TenantFilter + AvailableCmdlets = $true + } + if ($Request.Body.AsApp -eq $true) { + $ExoRequest.AsApp = $true + } + if ($Request.Body.Compliance -eq $true) { + $ExoRequest.Compliance = $true + } + $Results = New-ExoRequest @ExoRequest + $Body = [PSCustomObject]@{ + Results = $Results | Select-Object @{ Name = 'Cmdlet'; Expression = { $_ } } + Metadata = @{ + Count = ($Results | Measure-Object).Count } - $Results = New-ExoRequest @ExoRequest + } + } else { + if ($AllowedVerbs -notcontains $Verb) { $Body = [PSCustomObject]@{ - Results = $Results | Select-Object @{ Name = 'Cmdlet'; Expression = { $_ } } - Metadata = @{ - Count = ($Results | Measure-Object).Count - } - } - } else { - if ($AllowedVerbs -notcontains $Verb) { - $Body = [pscustomobject]@{ - Results = "Invalid cmdlet: $Cmdlet" - } - return ([HttpResponseContext]@{ - StatusCode = [HttpStatusCode]::BadRequest - Body = $Body - }) - return - } - $ExoParams = @{ - Cmdlet = $Cmdlet - cmdParams = $cmdParams - tenantid = $TenantFilter + Results = "Invalid cmdlet: $Cmdlet" } + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = $Body + }) + return + } - if ($Request.Body.Select) { - $ExoParams.Select = $Request.Body.Select - } + $ExoParams = @{ + Cmdlet = $Cmdlet + cmdParams = $cmdParams + tenantid = $TenantFilter + } - if ($Request.Body.UseSystemMailbox -eq $true) { - $ExoParams.useSystemMailbox = $true - } + if ($Request.Body.Select) { + $ExoParams.Select = $Request.Body.Select + } - if ($Request.Body.Anchor) { - $ExoParams.Anchor = $Request.Body.Anchor - } + if ($Request.Body.UseSystemMailbox -eq $true) { + $ExoParams.useSystemMailbox = $true + } - if ($Request.Body.Compliance -eq $true) { - $ExoParams.Compliance = $true - } + if ($Request.Body.Anchor) { + $ExoParams.Anchor = $Request.Body.Anchor + } - if ($Request.Body.AsApp -eq $true) { - $ExoParams.AsApp = $true - } + if ($Request.Body.Compliance -eq $true) { + $ExoParams.Compliance = $true + } - try { - $Results = New-ExoRequest @ExoParams - $Body = [pscustomobject]@{ - Results = $Results - } - } catch { - $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - $Body = [pscustomobject]@{ - Results = @(@{ Error = $ErrorMessage }) - } - } + if ($Request.Body.AsApp -eq $true) { + $ExoParams.AsApp = $true } - } else { - $Body = [pscustomobject]@{ - Results = "Invalid tenant: $TenantFilter" + + try { + $Results = New-ExoRequest @ExoParams + $Body = [pscustomobject]@{ + Results = $Results + } + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + $Body = [pscustomobject]@{ + Results = @(@{ Error = $ErrorMessage }) + } } } } catch { From ac3384171d30463c06baff35679b63d33959585a Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 1 Jun 2026 21:05:23 +0200 Subject: [PATCH 100/202] add featureflag for mcp --- Config/FeatureFlags.json | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Config/FeatureFlags.json b/Config/FeatureFlags.json index 1216ac071689..f7783e114184 100644 --- a/Config/FeatureFlags.json +++ b/Config/FeatureFlags.json @@ -44,5 +44,18 @@ "/cipp/advanced/worker-health" ], "Hidden": true + }, + { + "Id": "MCPServer", + "Name": "MCP Server", + "Description": "Model Context Protocol (MCP) server endpoint that exposes CIPP's read-only API surface as tools for AI clients. Disabled by default; enable to allow MCP access.", + "Enabled": false, + "AllowUserToggle": true, + "Timers": [], + "Endpoints": [ + "ExecMcp" + ], + "Pages": [], + "Hidden": false } ] From 235bd855f2eb1e041198a767926bd70b5f279849 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 1 Jun 2026 15:07:38 -0400 Subject: [PATCH 101/202] fix: validate and sanitize msp/choco app params M-007/M-009 --- .../Public/ConvertTo-CIPPSafePwshArg.ps1 | 31 ++++++++++ .../Applications/Invoke-AddChocoApp.ps1 | 11 ++-- .../Applications/Invoke-AddMSPApp.ps1 | 58 +++++++++++++++---- 3 files changed, 85 insertions(+), 15 deletions(-) create mode 100644 Modules/CIPPCore/Public/ConvertTo-CIPPSafePwshArg.ps1 diff --git a/Modules/CIPPCore/Public/ConvertTo-CIPPSafePwshArg.ps1 b/Modules/CIPPCore/Public/ConvertTo-CIPPSafePwshArg.ps1 new file mode 100644 index 000000000000..b859b8a0ac00 --- /dev/null +++ b/Modules/CIPPCore/Public/ConvertTo-CIPPSafePwshArg.ps1 @@ -0,0 +1,31 @@ +function ConvertTo-CIPPSafePwshArg { + <# + .SYNOPSIS + Escapes a value for safe use as a single PowerShell command-line argument. + .DESCRIPTION + Wraps values in single quotes and escapes embedded single quotes by doubling them. + Also strips CR/LF to prevent multiline argument injection when command lines are + generated as strings for downstream execution (for example, Intune install commands). + .PARAMETER Value + The value to encode as a PowerShell-safe argument token. + .EXAMPLE + $SafeValue = ConvertTo-CIPPSafePwshArg -Value $Request.Body.customArguments + $Command = "powershell.exe -ExecutionPolicy Bypass .\\install.ps1 -CustomArguments $SafeValue" + #> + [CmdletBinding()] + [OutputType([string])] + param( + [AllowNull()] + [AllowEmptyString()] + [string]$Value + ) + + if ($null -eq $Value) { + return "''" + } + + $EscapedValue = $Value -replace "'", "''" + $EscapedValue = $EscapedValue -replace "`r|`n", ' ' + + return "'{0}'" -f $EscapedValue +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddChocoApp.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddChocoApp.ps1 index de8bdb8a5172..948a4af8af0e 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddChocoApp.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddChocoApp.ps1 @@ -19,14 +19,17 @@ function Invoke-AddChocoApp { $intuneBody.displayName = $ChocoApp.ApplicationName $intuneBody.installExperience.runAsAccount = if ($ChocoApp.InstallAsSystem) { 'system' } else { 'user' } $intuneBody.installExperience.deviceRestartBehavior = if ($ChocoApp.DisableRestart) { 'suppress' } else { 'allow' } - $intuneBody.installCommandLine = "powershell.exe -ExecutionPolicy Bypass .\Install.ps1 -InstallChoco -Packagename $($ChocoApp.PackageName)" + $PackageNameArg = ConvertTo-CIPPSafePwshArg -Value ([string]$ChocoApp.PackageName) + $intuneBody.installCommandLine = "powershell.exe -ExecutionPolicy Bypass .\Install.ps1 -InstallChoco -Packagename $PackageNameArg" if ($ChocoApp.customrepo) { - $intuneBody.installCommandLine = $intuneBody.installCommandLine + " -CustomRepo $($ChocoApp.CustomRepo)" + $CustomRepoArg = ConvertTo-CIPPSafePwshArg -Value ([string]$ChocoApp.CustomRepo) + $intuneBody.installCommandLine = $intuneBody.installCommandLine + " -CustomRepo $CustomRepoArg" } if ($ChocoApp.customArguments) { - $intuneBody.installCommandLine = $intuneBody.installCommandLine + " -CustomArguments '$($ChocoApp.customArguments)'" + $CustomArgumentsArg = ConvertTo-CIPPSafePwshArg -Value ([string]$ChocoApp.customArguments) + $intuneBody.installCommandLine = $intuneBody.installCommandLine + " -CustomArguments $CustomArgumentsArg" } - $intuneBody.UninstallCommandLine = "powershell.exe -ExecutionPolicy Bypass .\Uninstall.ps1 -Packagename $($ChocoApp.PackageName)" + $intuneBody.UninstallCommandLine = "powershell.exe -ExecutionPolicy Bypass .\Uninstall.ps1 -Packagename $PackageNameArg" $intuneBody.detectionRules[0].path = "$($ENV:SystemDrive)\programdata\chocolatey\lib" $intuneBody.detectionRules[0].fileOrFolderName = "$($ChocoApp.PackageName)" diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddMSPApp.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddMSPApp.ps1 index fe750f11e6fb..2c703956fad9 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddMSPApp.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddMSPApp.ps1 @@ -13,47 +13,75 @@ function Invoke-AddMSPApp { $RMMApp = $Request.Body + $RmmName = [string]$RMMApp.RMMName.value + $SupportedRmmApps = @('datto', 'ninja', 'Huntress', 'syncro', 'NCentral', 'automate', 'cwcommand') + + if ([string]::IsNullOrWhiteSpace($RmmName) -or $SupportedRmmApps -notcontains $RmmName) { + $Message = "Unknown MSP app type '{0}'. Supported values: {1}" -f $RmmName, ($SupportedRmmApps -join ', ') + Write-LogMessage -headers $Headers -API $APIName -message $Message -Sev 'Warning' + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = [PSCustomObject]@{ Results = @($Message) } + }) + } + $AssignTo = $Request.Body.AssignTo -eq 'customGroup' ? $Request.Body.CustomGroup : $Request.Body.AssignTo - $intuneBody = Get-Content "AddMSPApp\$($RMMApp.RMMName.value).app.json" | ConvertFrom-Json + $intuneBody = Get-Content "AddMSPApp\$RmmName.app.json" | ConvertFrom-Json $intuneBody.displayName = $RMMApp.DisplayName $AllowedTenants = Test-CIPPAccess -Request $Request -TenantList $Tenants = $Request.Body.selectedTenants | Where-Object { $AllowedTenants -contains $_.customerId -or $AllowedTenants -contains 'AllTenants' } + $SuccessCount = 0 + $ErrorCount = 0 $Results = foreach ($Tenant in $Tenants) { $InstallParams = [PSCustomObject]$RMMApp.params - switch ($RMMApp.RMMName.value) { + switch ($RmmName) { 'datto' { Write-Host 'Processing Datto installation' - $installCommandLine = "powershell.exe -ExecutionPolicy Bypass .\install.ps1 -URL $($InstallParams.DattoURL) -GUID $($InstallParams.DattoGUID."$($Tenant.customerId)")" + $DattoUrl = ConvertTo-CIPPSafePwshArg -Value ([string]$InstallParams.DattoURL) + $DattoGuid = ConvertTo-CIPPSafePwshArg -Value ([string]$InstallParams.DattoGUID."$($Tenant.customerId)") + $installCommandLine = "powershell.exe -ExecutionPolicy Bypass .\install.ps1 -URL $DattoUrl -GUID $DattoGuid" $uninstallCommandLine = 'powershell.exe -ExecutionPolicy Bypass .\uninstall.ps1' } 'ninja' { Write-Host 'Processing Ninja installation' - $installCommandLine = "powershell.exe -ExecutionPolicy Bypass .\install.ps1 -InstallParam $($RMMApp.PackageName)" + $NinjaPackage = ConvertTo-CIPPSafePwshArg -Value ([string]$RMMApp.PackageName) + $installCommandLine = "powershell.exe -ExecutionPolicy Bypass .\install.ps1 -InstallParam $NinjaPackage" $uninstallCommandLine = 'powershell.exe -ExecutionPolicy Bypass .\uninstall.ps1' } 'Huntress' { - $installCommandLine = "powershell.exe -ExecutionPolicy Bypass .\install.ps1 -OrgKey $($InstallParams.Orgkey."$($Tenant.customerId)") -acctkey $($InstallParams.AccountKey)" + $HuntressOrgKey = ConvertTo-CIPPSafePwshArg -Value ([string]$InstallParams.Orgkey."$($Tenant.customerId)") + $HuntressAccountKey = ConvertTo-CIPPSafePwshArg -Value ([string]$InstallParams.AccountKey) + $installCommandLine = "powershell.exe -ExecutionPolicy Bypass .\install.ps1 -OrgKey $HuntressOrgKey -acctkey $HuntressAccountKey" $uninstallCommandLine = 'powershell.exe -ExecutionPolicy Bypass .\install.ps1 -Uninstall' } 'syncro' { - $installCommandLine = "powershell.exe -ExecutionPolicy Bypass .\install.ps1 -URL $($InstallParams.ClientURL."$($Tenant.customerId)")" + $SyncroUrl = ConvertTo-CIPPSafePwshArg -Value ([string]$InstallParams.ClientURL."$($Tenant.customerId)") + $installCommandLine = "powershell.exe -ExecutionPolicy Bypass .\install.ps1 -URL $SyncroUrl" $uninstallCommandLine = 'powershell.exe -ExecutionPolicy Bypass .\uninstall.ps1' } 'NCentral' { - $installCommandLine = "powershell.exe -ExecutionPolicy Bypass .\install.ps1 -InstallParam $($RMMApp.PackageName)" + $NCentralPackage = ConvertTo-CIPPSafePwshArg -Value ([string]$RMMApp.PackageName) + $installCommandLine = "powershell.exe -ExecutionPolicy Bypass .\install.ps1 -InstallParam $NCentralPackage" $uninstallCommandLine = 'powershell.exe -ExecutionPolicy Bypass .\uninstall.ps1' } 'automate' { - $installCommandLine = "c:\windows\sysnative\windowspowershell\v1.0\powershell.exe -ExecutionPolicy Bypass .\install.ps1 -Server $($InstallParams.Server) -InstallerToken $($InstallParams.InstallerToken."$($Tenant.customerId)") -LocationID $($InstallParams.LocationID."$($Tenant.customerId)")" - $uninstallCommandLine = "c:\windows\sysnative\windowspowershell\v1.0\powershell.exe -ExecutionPolicy Bypass .\uninstall.ps1 -Server $($InstallParams.Server)" + $AutomateServer = ConvertTo-CIPPSafePwshArg -Value ([string]$InstallParams.Server) + $AutomateInstallerToken = ConvertTo-CIPPSafePwshArg -Value ([string]$InstallParams.InstallerToken."$($Tenant.customerId)") + $AutomateLocationId = ConvertTo-CIPPSafePwshArg -Value ([string]$InstallParams.LocationID."$($Tenant.customerId)") + $installCommandLine = "c:\windows\sysnative\windowspowershell\v1.0\powershell.exe -ExecutionPolicy Bypass .\install.ps1 -Server $AutomateServer -InstallerToken $AutomateInstallerToken -LocationID $AutomateLocationId" + $uninstallCommandLine = "c:\windows\sysnative\windowspowershell\v1.0\powershell.exe -ExecutionPolicy Bypass .\uninstall.ps1 -Server $AutomateServer" $DetectionScript = (Get-Content 'AddMSPApp\automate.detection.ps1' -Raw) -replace '##SERVER##', $InstallParams.Server $intuneBody.detectionRules[0].scriptContent = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($DetectionScript)) } 'cwcommand' { - $installCommandLine = "powershell.exe -ExecutionPolicy Bypass .\install.ps1 -Url $($InstallParams.ClientURL."$($Tenant.customerId)")" + $CwClientUrl = ConvertTo-CIPPSafePwshArg -Value ([string]$InstallParams.ClientURL."$($Tenant.customerId)") + $installCommandLine = "powershell.exe -ExecutionPolicy Bypass .\install.ps1 -Url $CwClientUrl" $uninstallCommandLine = 'powershell.exe -ExecutionPolicy Bypass .\uninstall.ps1' } + default { + throw "Unknown MSP app type '$RmmName'" + } } $intuneBody.installCommandLine = $installCommandLine $intuneBody.UninstallCommandLine = $uninstallCommandLine @@ -76,18 +104,26 @@ function Invoke-AddMSPApp { PartitionKey = 'apps' status = 'Not Deployed yet' } + $SuccessCount++ "Successfully added MSP App for $($Tenant.defaultDomainName) to queue. " Write-LogMessage -headers $Headers -API $APIName -tenant $Tenant.defaultDomainName -message "MSP Application $($intuneBody.DisplayName) added to queue" -Sev 'Info' } catch { + $ErrorCount++ Write-LogMessage -headers $Headers -API $APIName -tenant $Tenant.defaultDomainName -message "Failed to add MSP Application $($intuneBody.DisplayName) to queue" -Sev 'Error' "Failed to add MSP app for $($Tenant.defaultDomainName) to queue" } } + $StatusCode = [HttpStatusCode]::OK + if ($ErrorCount -gt 0 -and $SuccessCount -eq 0) { + $StatusCode = [HttpStatusCode]::InternalServerError + } elseif ($ErrorCount -gt 0) { + $StatusCode = [HttpStatusCode]::MultiStatus + } $body = [PSCustomObject]@{'Results' = $Results } return ([HttpResponseContext]@{ - StatusCode = [HttpStatusCode]::OK + StatusCode = $StatusCode Body = $body }) From 55a97db2c4165b456b2c210d9bf0eff3c5f364bd Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 1 Jun 2026 23:31:28 +0200 Subject: [PATCH 102/202] added logging --- .../Tenant/Administration/Invoke-ExecOffboardTenant.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecOffboardTenant.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecOffboardTenant.ps1 index f13ff094018c..6c401e63196a 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecOffboardTenant.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecOffboardTenant.ps1 @@ -135,6 +135,7 @@ function Invoke-ExecOffboardTenant { Write-LogMessage -headers $Headers -API $APIName -message "App $($_.label) was removed" -Sev 'Info' -tenant $TenantFilter } catch { $Errors.Add("Failed to removed app $($_.label)") + Write-LogMessage -headers $Headers -API $APIName -message "Failed to remove app $($_.label)" -Sev 'Error' -tenant $TenantFilter } } } From 0c54cd16abf70bbebcd5711acf308e66bdfc8847 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 1 Jun 2026 18:21:07 -0400 Subject: [PATCH 103/202] fix: move New-CIPPCoreRequest back to CIPPCore --- .../Public/Entrypoints/HTTP Functions/New-CippCoreRequest.ps1 | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Modules/{CIPPHTTP => CIPPCore}/Public/Entrypoints/HTTP Functions/New-CippCoreRequest.ps1 (100%) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/New-CippCoreRequest.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/New-CippCoreRequest.ps1 similarity index 100% rename from Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/New-CippCoreRequest.ps1 rename to Modules/CIPPCore/Public/Entrypoints/HTTP Functions/New-CippCoreRequest.ps1 From 2492ad4ffa3c17c8d79a8e8e7b0e09e6e969a0ba Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 2 Jun 2026 00:44:44 +0200 Subject: [PATCH 104/202] customsubject fix --- .../CIPPCore/Public/Webhooks/Invoke-CIPPWebhookProcessing.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Webhooks/Invoke-CIPPWebhookProcessing.ps1 b/Modules/CIPPCore/Public/Webhooks/Invoke-CIPPWebhookProcessing.ps1 index aa2ddec7fcea..161463bcd458 100644 --- a/Modules/CIPPCore/Public/Webhooks/Invoke-CIPPWebhookProcessing.ps1 +++ b/Modules/CIPPCore/Public/Webhooks/Invoke-CIPPWebhookProcessing.ps1 @@ -122,7 +122,7 @@ function Invoke-CippWebhookProcessing { $LogId = $Data.Id $AuditLogLink = '{0}/tenant/administration/audit-logs/log?logId={1}&tenantFilter={2}' -f $CIPPURL, $LogId, $Tenant.defaultDomainName - $GenerateEmail = New-CIPPAlertTemplate -format 'html' -data $Data -ActionResults $ActionResults -CIPPURL $CIPPURL -Tenant $Tenant.defaultDomainName -AuditLogLink $AuditLogLink -AlertComment $AlertComment + $GenerateEmail = New-CIPPAlertTemplate -format 'html' -data $Data -ActionResults $ActionResults -CIPPURL $CIPPURL -Tenant $Tenant.defaultDomainName -AuditLogLink $AuditLogLink -AlertComment $AlertComment -CustomSubject $Data.CIPPCustomSubject Write-Host 'Going to create the content' foreach ($action in $ActionList ) { From 287521447902e5611303f14e46045279b0abeca7 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 2 Jun 2026 00:47:14 +0200 Subject: [PATCH 105/202] unique --- .../Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 index 7a58a86a9b70..5c6753fc6de9 100644 --- a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 +++ b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 @@ -1669,7 +1669,7 @@ function Invoke-NinjaOneTenantSync { 'Tenant ID' = $Customer.customerId 'Creation Date' = $TenantDetails.createdDateTime 'Domains' = $customerDomains - 'Admin Users' = ($AdminUsers | ForEach-Object { "$($_.DisplayName)" }) -join ', ' + 'Admin Users' = ($AdminUsers | Select-Object -Property DisplayName -Unique | ForEach-Object { "$($_.DisplayName)" }) -join ', ' } From 3f9dbd9fcbc26fb671950ba00162f89ebade7472 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 2 Jun 2026 07:30:18 +0800 Subject: [PATCH 106/202] new licence report endpoint and revert old endpoint --- .../HTTP Functions/Invoke-ListLicenses.ps1 | 53 +++++++++++++++++++ ...nses.ps1 => Invoke-ListLicensesReport.ps1} | 2 +- 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListLicenses.ps1 rename Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Reports/{Invoke-ListLicenses.ps1 => Invoke-ListLicensesReport.ps1} (98%) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListLicenses.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListLicenses.ps1 new file mode 100644 index 000000000000..c241e26c0736 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListLicenses.ps1 @@ -0,0 +1,53 @@ +function Invoke-ListLicenses { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Tenant.Directory.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + # Interact with query parameters or the body of the request. + $TenantFilter = $Request.Query.tenantFilter + if ($TenantFilter -ne 'AllTenants') { + $GraphRequest = Get-CIPPLicenseOverview -TenantFilter $TenantFilter | ForEach-Object { + $_ + } + } else { + $Table = Get-CIPPTable -TableName cachelicenses + $Rows = Get-CIPPAzDataTableEntity @Table | Where-Object -Property Timestamp -GT (Get-Date).AddHours(-1) + if (!$Rows) { + $GraphRequest = [PSCustomObject]@{ + Tenant = 'Loading data for all tenants. Please check back in 1 minute' + License = 'Loading data for all tenants. Please check back in 1 minute' + } + $Tenants = Get-Tenants -IncludeErrors + + if (($Tenants | Measure-Object).Count -gt 0) { + $Queue = New-CippQueueEntry -Name 'Licenses (All Tenants)' -TotalTasks ($Tenants | Measure-Object).Count + $Tenants = $Tenants | Select-Object customerId, defaultDomainName, @{Name = 'QueueId'; Expression = { $Queue.RowKey } }, @{Name = 'FunctionName'; Expression = { 'ListLicensesQueue' } }, @{Name = 'QueueName'; Expression = { $_.defaultDomainName } } + $InputObject = [PSCustomObject]@{ + OrchestratorName = 'ListLicensesOrchestrator' + Batch = @($Tenants) + SkipLog = $true + } + #Write-Host ($InputObject | ConvertTo-Json) + $InstanceId = Start-CIPPOrchestrator -InputObject $InputObject + Write-Host "Started permissions orchestration with ID = '$InstanceId'" + } + } else { + $GraphRequest = $Rows | ForEach-Object { + $LicenseData = $_.License | ConvertFrom-Json -ErrorAction SilentlyContinue + foreach ($License in $LicenseData) { + $License + } + } + } + } + + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = @($GraphRequest) + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Reports/Invoke-ListLicenses.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Reports/Invoke-ListLicensesReport.ps1 similarity index 98% rename from Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Reports/Invoke-ListLicenses.ps1 rename to Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Reports/Invoke-ListLicensesReport.ps1 index 3c89435426a2..9a896788636a 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Reports/Invoke-ListLicenses.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Reports/Invoke-ListLicensesReport.ps1 @@ -1,4 +1,4 @@ -function Invoke-ListLicenses { +function Invoke-ListLicensesReport { <# .FUNCTIONALITY Entrypoint From 3f842abc805d82fcd12f3d526b1dd66fa5cb9764 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 2 Jun 2026 10:13:12 -0400 Subject: [PATCH 107/202] fix: explicitly remove tenant from the table instead of using -cleanold --- .../Invoke-ExecOffboardTenant.ps1 | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecOffboardTenant.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecOffboardTenant.ps1 index 6c401e63196a..c84b15000f51 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecOffboardTenant.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecOffboardTenant.ps1 @@ -27,7 +27,7 @@ function Invoke-ExecOffboardTenant { } elseif ($TenantId -eq $env:TenantID) { $Errors.Add('You cannot offboard the CSP tenant') } else { - if ($request.body.RemoveCSPGuestUsers -eq $true) { + if ($Request.Body.RemoveCSPGuestUsers -eq $true) { # Delete guest users who's domains match the CSP tenants try { try { @@ -59,7 +59,7 @@ function Invoke-ExecOffboardTenant { } } - if ($request.body.RemoveCSPnotificationContacts -eq $true) { + if ($Request.Body.RemoveCSPnotificationContacts -eq $true) { # Remove all email addresses that match the CSP tenants domains from the contact properties in /organization try { try { @@ -104,7 +104,7 @@ function Invoke-ExecOffboardTenant { } - if ($request.body.RemoveDomainAnalyserData -eq $true) { + if ($Request.Body.RemoveDomainAnalyserData -eq $true) { # Remove all Domain Analyser data for this tenant try { $DomainTable = Get-CIPPTable -Table 'Domains' @@ -141,7 +141,7 @@ function Invoke-ExecOffboardTenant { } # All customer tenant specific actions ALWAYS have to be completed before this action! - if ($request.body.RemoveMultitenantCSPApps -eq $true) { + if ($Request.Body.RemoveMultitenantCSPApps -eq $true) { # Remove multi-tenant apps with the CSP tenant as origin try { $MultiTenantCSPApps = (New-GraphGETRequest -Uri "https://graph.microsoft.com/v1.0/servicePrincipals?`$count=true&`$select=displayName,appId,id,appOwnerOrganizationId&`$filter=appOwnerOrganizationId eq $($env:TenantID)" -tenantid $TenantFilter -ComplexFilter) @@ -162,7 +162,7 @@ function Invoke-ExecOffboardTenant { } } $ClearCache = $false - if ($request.body.TerminateGDAP -eq $true) { + if ($Request.Body.TerminateGDAP -eq $true) { # Terminate GDAP relationships $ClearCache = $true try { @@ -186,7 +186,7 @@ function Invoke-ExecOffboardTenant { } } - if ($request.body.TerminateContract -eq $true) { + if ($Request.Body.TerminateContract -eq $true) { # Terminate contract relationship try { $null = (New-GraphPostRequest -type 'PATCH' -body '{ "relationshipToPartner": "none" }' -Uri "https://api.partnercenter.microsoft.com/v1/customers/$TenantFilter" -ContentType 'application/json' -scope 'https://api.partnercenter.microsoft.com/user_impersonation' -tenantid $env:TenantID) @@ -199,9 +199,20 @@ function Invoke-ExecOffboardTenant { } } - if ($ClearCache) { - $null = Get-Tenants -CleanOld - $Results.Add('Tenant cache has been cleared') + if ($ClearCache -eq $true) { + try { + $TenantsTable = Get-CippTable -tablename 'Tenants' + $TenantRow = Get-CIPPAzDataTableEntity @TenantsTable -Filter "PartitionKey eq 'Tenants' and RowKey eq '$TenantId'" -Property RowKey, PartitionKey, customerId, displayName + if ($TenantRow) { + Remove-AzDataTableEntity -Force @TenantsTable -Entity $TenantRow + $Results.Add("$($Tenant.displayName) ($TenantId) has been deleted from CIPP") + Write-LogMessage -headers $Headers -API $APIName -message "Tenant $($Tenant.displayName) ($TenantId) deleted from CIPP" -Sev 'Info' -tenant $TenantFilter + } else { + $Results.Add("Tenant $TenantId was not found in CIPP tenant list") + } + } catch { + $Errors.Add("Failed to delete tenant from CIPP: $($_.Exception.message)") + } } Write-LogMessage -headers $Headers -API $APIName -message 'Offboarding completed' -Sev 'Info' -tenant $TenantFilter From 55ddb18b23e73d0ed21f12c7ceadb40d87822c7a Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 3 Jun 2026 07:12:50 +0800 Subject: [PATCH 108/202] Update Invoke-ExecTestRun.ps1 --- .../HTTP Functions/Invoke-ExecTestRun.ps1 | 75 ++++++++++++++----- 1 file changed, 55 insertions(+), 20 deletions(-) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ExecTestRun.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ExecTestRun.ps1 index 47eac1c03f7d..696f307ca965 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ExecTestRun.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ExecTestRun.ps1 @@ -12,33 +12,68 @@ function Invoke-ExecTestRun { try { $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter - Write-LogMessage -API $APIName -tenant $TenantFilter -message "Starting data collection and test run for tenant: $TenantFilter" -sev Info - $Batch = @( - @{ - FunctionName = 'CIPPDBCacheData' - TenantFilter = $TenantFilter - QueueName = "Cache - $TenantFilter" + $Mode = ($Request.Query.mode ?? $Request.Body.mode ?? 'both').ToString().ToLower() + if ($Mode -notin @('both', 'cache', 'tests')) { $Mode = 'both' } + + switch ($Mode) { + 'tests' { + Write-LogMessage -API $APIName -tenant $TenantFilter -message "Starting tests-only run for tenant: $TenantFilter" -sev Info + $InstanceId = Start-CIPPDBTestsRun -TenantFilter $TenantFilter -Force + $ResultMessage = "Successfully started test run for $TenantFilter" + } + 'cache' { + Write-LogMessage -API $APIName -tenant $TenantFilter -message "Starting cache-only collection for tenant: $TenantFilter" -sev Info + $Batch = @( + @{ + FunctionName = 'CIPPDBCacheData' + TenantFilter = $TenantFilter + QueueName = "Cache - $TenantFilter" + } + ) + $InputObject = [PSCustomObject]@{ + OrchestratorName = "TestDataCollection-$TenantFilter" + Batch = $Batch + SkipLog = $false + PostExecution = @{ + FunctionName = 'CIPPDBCacheApplyBatch' + Parameters = @{ + TenantFilter = $TenantFilter + } + } + } + $InstanceId = Start-CIPPOrchestrator -InputObject $InputObject + $ResultMessage = "Successfully started cache collection for $TenantFilter" } - ) - $InputObject = [PSCustomObject]@{ - OrchestratorName = "TestDataCollectionAndRun-$TenantFilter" - Batch = $Batch - SkipLog = $false - PostExecution = @{ - FunctionName = 'CIPPDBCacheApplyBatch' - Parameters = @{ - TestRun = $true - TenantFilter = $TenantFilter + default { + Write-LogMessage -API $APIName -tenant $TenantFilter -message "Starting data collection and test run for tenant: $TenantFilter" -sev Info + $Batch = @( + @{ + FunctionName = 'CIPPDBCacheData' + TenantFilter = $TenantFilter + QueueName = "Cache - $TenantFilter" + } + ) + $InputObject = [PSCustomObject]@{ + OrchestratorName = "TestDataCollectionAndRun-$TenantFilter" + Batch = $Batch + SkipLog = $false + PostExecution = @{ + FunctionName = 'CIPPDBCacheApplyBatch' + Parameters = @{ + TestRun = $true + TenantFilter = $TenantFilter + } + } } + $InstanceId = Start-CIPPOrchestrator -InputObject $InputObject + $ResultMessage = "Successfully started data collection and test run for $TenantFilter" } } - $InstanceId = Start-CIPPOrchestrator -InputObject $InputObject - $StatusCode = [HttpStatusCode]::OK - $Body = [PSCustomObject]@{ Results = "Successfully started data collection and test run for $TenantFilter" } + $Body = [PSCustomObject]@{ Results = $ResultMessage } - Write-LogMessage -API $APIName -tenant $TenantFilter -message "Data collection and test run orchestration started. Instance ID: $InstanceId" -sev Info + Write-LogMessage -API $APIName -tenant $TenantFilter -message "Mode '$Mode' orchestration started. Instance ID: $InstanceId" -sev Info } catch { $ErrorMessage = Get-CippException -Exception $_ From 1167ff5bd293fccad0a5c51919c85449982636fc Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 3 Jun 2026 08:09:45 +0800 Subject: [PATCH 109/202] caching bump --- Shared/CIPPSharp/CIPPTestDataCache.cs | 25 ++++++++++++++++++++++--- Shared/CIPPSharp/bin/CIPPSharp.dll | Bin 41472 -> 41984 bytes 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/Shared/CIPPSharp/CIPPTestDataCache.cs b/Shared/CIPPSharp/CIPPTestDataCache.cs index 496a631922d9..282ebee87b42 100644 --- a/Shared/CIPPSharp/CIPPTestDataCache.cs +++ b/Shared/CIPPSharp/CIPPTestDataCache.cs @@ -19,8 +19,8 @@ namespace CIPP public static class TestDataCache { // ── Configuration ── - private static long _maxBytes = 50L * 1024 * 1024; // 50 MB default - private static TimeSpan _ttl = TimeSpan.FromMinutes(1); + private static long _maxBytes = 100L * 1024 * 1024; // 100 MB default + private static TimeSpan _ttl = TimeSpan.FromMinutes(5); // ── State ── private static readonly ConcurrentDictionary _cache = new(); @@ -52,7 +52,7 @@ public CacheEntry(object? value, long sizeBytes, DateTime expiresUtc) } /// Configure the cache limits. Call before first use or between test runs. - public static void Configure(long maxBytes = 50 * 1024 * 1024, int ttlSeconds = 60) + public static void Configure(long maxBytes = 100L * 1024 * 1024, int ttlSeconds = 300) { _maxBytes = maxBytes; _ttl = TimeSpan.FromSeconds(ttlSeconds); @@ -172,6 +172,25 @@ public static void Clear() Interlocked.Exchange(ref _evictions, 0); } + /// + /// Remove all entries belonging to a single tenant. Cache keys are formatted + /// as "tenantFilter|type", so we match by the "tenantFilter|" prefix. + /// + public static int ClearTenant(string tenantFilter) + { + if (string.IsNullOrWhiteSpace(tenantFilter)) return 0; + var prefix = tenantFilter + "|"; + int removed = 0; + // Snapshot keys to avoid mutating while iterating the concurrent dictionary + var matchingKeys = _cache.Keys.Where(k => k.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)).ToList(); + foreach (var key in matchingKeys) + { + RemoveEntry(key); + removed++; + } + return removed; + } + public static int Count => _cache.Count; public static long CurrentBytes => Interlocked.Read(ref _currentBytes); public static double CurrentMB => Math.Round(Interlocked.Read(ref _currentBytes) / (1024.0 * 1024.0), 2); diff --git a/Shared/CIPPSharp/bin/CIPPSharp.dll b/Shared/CIPPSharp/bin/CIPPSharp.dll index f74704b1943c28aa883a83925eb5d7353c3e74b8..a6afa3419dd6982034d9bd1526f591ede1bb183a 100644 GIT binary patch delta 15241 zcmcJ033yahw(i=es!q*wRpwNxQVhiqNFWG7)I?CB0g*vvQW6Lt2$T?LQHDw&f+8sK zASi=Ei>=a52-<+CM6hjI?W;Jn1JXp`+IH&|yTRLvjqhKpj*8mveeZtXdj<9H^{=(| z+H3E#&pzkuI%zzhHU3^(eQRm3UC)on{2ZmyxLcJIZG=M4D*v#jW!qK0`Fn_lv0@+5 z4VIQf*mIP~13n&xA|s)7HO8opu6Nf%6~}j@u|joG_ZiQqj_Oy&5mlQ1C>;4S5MPaK zR6l~~pHqpH(Yn6(jw`fx{pv2GQ>d#ljH^Nys5Qo#P&ecAkj;2Glu~Prlc8>Eolz40 zOxLKHzoMilA zt}#n$+`u(v1KOA6c33dTDdiqJ@OkR)D zolGq0C%CYtw{wwdL`YYNQcWn+JB(rZccS{A^PALfchnWEQ~Go8D!a-^#9~HKQH9!N zTwYYGCK^u^^-z0_kBUaA-A3=?KLc}Pr!$4+7WyT|HcAw1XhltzmRf|sUA3Ifx;7_a zryEjFf-kxoN$0p1SEArNT>4r4MYuOS1v%v->XPyU(ochWx&%U6>KQg%2^V|rXgBz8 z!OK2A;tOeKb9xXJrdL2weAwO@2Q$D2cMug%Aw(kTmThL25h+PUcEhc6!D3XgcJ7hs zi6XVfxT0jDUkp@OuF=b=lVh}&bkd)Nb9%3FvLx!pFu20Dpe>Sm-U!4a=f42nSdHpI z>zoJBt+Y7kK|PY%2UFK>L4D2{){A2eJL+?64ptZw;=?n!7$L39k~6(?st&!f6jt1< z%|krp(!1WkL+5hqX$%RvU+hTlhv2n4b5fJwn?3+$Prn2#?Cx-+Z-Y?w!I6k#-o>aN zMn+xjL8}bN;jCMko-;jcJJQ1*Nnvcd+%esfH+^i@G<_Tl1zI+8Bo#C+OH`>mqbU)d zD*cEqz6iN%X$*?1t3$J->XAX`?^C@}n|CQ1EgP!M!?7$ZyX42I=X{^KC3o}>$(uQU zBsm*r5@mJyJYjy1He8E&P$Q{>+^K@aJa8uQE=KoE(pA$ti=&qQ0~}@G-1ahP?dG5j zFk6-g&h#IQ+e%B4?E8HpuYeq}bT~q0HaoM~yi!_KQ-EwzTVU^yojR{VbmdHUH@pVH zqlfJ|VMiY*DQFKm9_WpZhn*p3>W{FNEX>PE|A%o&vP6F!G`-81o-CM=6)|!_Pii8H zu+JX!jdnYN?vOwI2K>tM8}5X@ebAj=&UQacL4W!XOhJG9^sTH51OvIF`Dp8`!hF{e zBYnHcS90kYonB>}OcrPQAg7RiHp-=$?}IkdYgTmAzLt%dWWde71GbGrvLA% z{V7{b=W52jcF}&fpwagF9n;d+vEWQk=K`~N2AB33Npu3A@|tmZDL!M zxws1U^b!^w>30!v@fs+w9#8ME*no6a-v?X6d$6=`WK$}{i&~f$wPsj46H&Na(QuUf zi4#@p(;InNGP@Y5PF;rDjl4uPyw6f4OCPY*i>1Rm0v$?}`p_uvcvV-P4L=*4&cD#+ zM6u*78U=qXm4~XGyEU{JPj#%y9AQ%GSE=uXKmSbg|H|~^$RoJX!DtL6TN;jj*KivP zGAvl7s1TJ$0z#+rdkQFf(b&jj3abM>YV!qtDyz0MT@@NYi5o*EbkWKL<#1!UVD0tXOD#RobFz%202*Dvlq(4+JXhBT;*3} zyxb+3IRnS`IqSpT)HjgAzT8v}&(yO_{9Wp>5`D`eR-idoV?q_CN#S%K6byvjqDU$5 za0qJ_zG)`G(3rG_GJjPqt`F^BAC2Zx7AVqIb`1uDDH{vEBc2XNjA>oVGCCXW0JCEb z=1#zzX?)pqLF({tO+k03)7)^5vW%_g(nyx8I&K?8XgcN#!xtl*Juh#P4ED`Ha0~ zh4~nMOxgf2oFB?h1zE@o<)uQ#$+Ec0HGR&4>B(^2rQ3(q0if~Bm_fj%dl%dtvAC8<0XVxd?n-*~25 zymKB7{)ZoFxQ$wX%Z)W0(-v}hHahS}$;46xcXiL}Tga6Q%{Gca(#5QacS#20=@^TN zU|}ecDuIY1jv|%M(<@;-(>*@mhdi%7Cr>%@KMPcfK&9D0r9TPOBO|9?w%es>LtDV( zkm`#QuR@m*V7QR}LD0@=@?10i&R@LuNWC)(ApudJ*4^_+r8k5)(Zp9@Yq~ zqWHS;#nMC9;A_Pfi@(^|?!XtV5iXB}fyd{snBn}<=}_=czD^f}81D`E^8Mt8i%y=P zJrmZcUFfZdPK`mv3k1gs{$23>0PF9U5Q_sG;(?eOn(+vv(+h&_gDgKLRyUe);juDo z*C|zUDBnku;;Ebvg(B~Ua%hUhmC)&=pCfz$|BOzp;a3WE`mHpetZbO2aWip^^A@zWUbS;=y8Y=QI-cr+z}V8Ku{OGEVaIf z@(BHBq@~D3tD*mIvw3Hd^}Bs7MLNBm$M}+uQ+!u3I?)XmWyYa`PRTHnJZiv3gW4WWWI1}B_DUm65M@G`$BRqhu8aKb&%R{uIxU$$sYk)dEgS2$| zV=<4-hovlkf_@-T8O+V%`4z5o1r+yj(v70IB+79bLX|vo36=0sJYDfv+0H^#B`m|V zgN><$aI*l`SBy>y3VT^>ZptME+w_t%dKHB!NljoU;>_}>o7m=wts~Wl?LHsdI%9`| zJy#3sK_k#;fD-v^yM(5a%7C7VF&#zAI6*>(k<4gXO_oG}tjMW~#xO&ehmhJTnnT-! zwMUOu(L8!oSP9PmDq2C0pHpuYJuS9&V!M-eontfTdDt=m+JrDwIR9IT&-KGNu~*Sr z+D{4kjpJfqtK(AOA}7lMC(HkGJOaGS`55pE=XT)Rj;DcE7yBF$pT9}nXGL!3WdBFS zf3o-wcC!B;oaGsnpkB^O;I}T1J3;Sz{6M#NJ~T_+EbsP&+(C-P3xT5r`w6~T!kXnG zpAq>tBEJKypl#t2;OwF#@SOt2J%V2pv)m+lt)PoLlc2x2xS2XPH+hY>C(K(5&I7g< zUW5kfV?BXq3K)BeJhF)8e-$#05zR7@{}tYGlK)pO>PvYI~;zPcloCQT{$y>-}o3Ohi1BmH*ODkfTP16+}lJwj9g`0iu6M3s^9^}l+Qnd4OpbhQ}un;4ms75qX#B~W8>#6qW zx4O@x4{a;mIdmhfW>kzjMBXIwC)#>w-Vx1tv=#D|it%M_2e8$`avv+>N-JyrnGsTJ zW7UH;R<((&q;iE~%_gb5R4PZTFG6#j^&Q|Y>j_{#+bOqB4FM1Eqg;ze(LUwy{Gjnd z)}m~^K0IIPbO3Wpr{5LlK!2<>!{ie`qr=Wl^TK|Nfycsl%o0PmvDOsuacdc{z}5q} z#MT?g%_u6i^aHkt#-j5m{=;&q=iK>}poN%KJaU(Na&Tggfb|#dDVTGEJv}ohZgq`^ zY6g&<>frP*nvCq~O6RL9;y62LnPY=vDDJ{Xn{0$HhMVr%EX(^muL}3#^M(D6J}T^G zx6n%_`_enYW~FaTmIyD$g?oB}@LA@VSTt12*l3PPe_s%@+304IJytN!W1}S|`>^m_ZtA zvJ>cFkTMfZ+BtSJ=ya3S6lK8XnCz3nX0V%0HaK=0*b4}vv!&$15^_hVzlTApRCk#{`pXoktgmK^qUAX|lL z%RA=jM3utU(t7V-Je{f5WZmpvdwS4OFtopxdgKSZeQA3oS6oXq`4O;_!dmi9=P|ps z7u%LO?)N5beQAox9`UaC^rf`Pu8%IK^XW#DHAc61&L`gC;dMnGE*@v{j62>11h%_W~+Ek1MXFJ~=7x#boWBWoP`|y#uJ;WZOc=JeScLleKre zWF1JAeX^d-;q$z|pj%D0I(*DCgkCincXudNSFvX%+uxzIL1?zWL+M#zYjHc&53aA9 z>`3fl?=WiYn{DD`Y>;<2ojRXc3w2K(^Nggq*ly#rpUjPTN0H}(EZbf(3~Y0jImRYN zd9S2Lvn=BnTR0kYmub959}o7n$!-$cVUx|v=Zc@0tSZL!PMhpR0W)=>H0U^9#LR25 ziU7wPBCLfLIU->FE@ICX%Ipo*f_kd6^lWsB_iEZ{vJJ5rUfg=su#IQQjo$H8KR`TX zUW}(!Vaps#3P#w*(?OFN1&YSgA(LGT+cosQ$!>t{8aigOJ{cIVrN5bUFpSsIDU;38 zAJ!+(Hzr%EFZ50z+a=umGRHvJYRPM|aj?}=j>&!vTOGwr_9Sd|)X`+~@<-SvQg@Rz z=Vug6q&_CCinUrNQMJh~jx~8F(IAtZC}_3T({PiWENJr9(^!*L1RmBW(?pY95LoD) zOxK(2cu}i$3N@JQ??p}CDYVdJtL@7&-l^1V(qG$Kz*d=TuYH;KI=b6rufuj7-Dk2{ zi4nGG^lOtXPAHm2Pnhhr=qB%U`mM?S9DN9EUzW+4c0Ijm+7|hjQ|5a5z@*Fk0lJ=! zn`{G`m_eVKY#W-GL8ndDudvlRlm2D0!G%rUnWSHuJun4DBW$zCXR=P%MwvyqChLa- zlcw@4lVs8v>Mt~V;-zWWxkkEL*iveur@U#p<~ydI^3JA7CVLLH2AXEFhhUpWX_Ng! z+vUBH_?I1`rF0aYH_-x+w>cOk7Z*&EtH?_yeOvR5q6doy&u$u71& z?`@=qO}5s0(7S}Tn@qDk?_ElJOg0^?nf9CP3EM&MGJ4fye}io~y`3?s2ie?8M@-gF z9rUiCk4^S4lDVBun(PiFvy#3tS&8*6?<(?LmhEAK^|<#A$}`!1>*wA(snTSZ*}nF+ zP_?i&)e{fqchT<$%Jr{J4fpo|eRm*-$r=r+AD(4wGiZ*mWe#5D*U-%-<5hkQEj1ah z@^{niCgWB9Zdzlq^{}m_jVAjIY-{Pk;d1@UUIEt8E-_|j!a6#Tb(Lwdj$ReE6ep|A zw~pR6!;A}&{T}+zWbH65@1YYW`=#COyN5nA*;HYtvkWnVuoaJx>)$d*t|MvNK;1#H zB+AORfzB6}O>G197q*n_s=&8_2A^ZwNFz;KiF)42yGM%d$7Ya5SNT}p65^=*QM9ax z^&epG$3iavt+@XwCj5NAAjZ%CU*`X`!pFFc)Mys0KMu4Jl{LB)dJCPBK;0nQ=n^!b z(`Wd_!A`@VanRF{e_WRn%4!&K{C^bwG4S$Lx6#Y{)=gOth{AJQhwsNgN zK*Iw@AYj3xMId0sV@4og!}CQTz)u!|fE`a6fq(-KB&L7ne^lYexpRo)c24w0KaWHn z_VKLr88kNf4`>wGlAO>s8CM%_yEVES4O?g>ZpgD%tpD5c*~y2Y`>ul3tPsGC_VPlu z8m2}=h-?zsa6ib3mSWES`d zSvhkq`M;Mh;w@A8C}R0s{2%Ev|A#>52F=RAwZ1~5)-zz%5b^SrK<;6*rtf)`kiY&LmhvuQTWIz^~o$u+=_3a3($$VDPY@ECa` zG>o(5A+kbS6T5>dWs9m(wx}v)i>gw#s48WPs#3P7`bnIA5@&$OnE^tE2pJ;sD3M2r z+)tVrAUH&Dl;A?y>sly#UCqRMUCqRMUCqRMUCpxB)l9tC)l9tC)y#Wcs#&(WR!aPp z5`Rrb$QmIlCG;A>3hm0|eD$b|M2*JdQKQ`vS*lu|_kLsyZ#hN4HKpgvZDVZ9w=XdIq>N<14mU6*n_m+ZG#YnXP?LIv<-jtceH~3AO`% zZRrZEvbM;0v}ofT_o#cdKZa_o2k^k5(_4bO(d0sCKF?cXeG90gWAO-gZ*vLbZIM;h z3hfo|{nkgcQ}N7JYo)q7^pthG8d&-)a72Eqb-OmQ^d;yYg1%B|kt$$Y-j~*X>TG#}fpDz+NeIe2vs?|$p9?|TMWr(va^ck#Y5;|q- zmDQ1>bGVp3L;6~;crfP*&Jg4jyXzI&s@TPPy=smP)SIQRv$fOyvHEQ7mBf5~B}S-4 zUn3*524{8>sdJCFYdgHZ(py!1{=@n~?Z(J+z*xb4{VVmd{}1}X44t2RT|Y?UlJ5d9 zgS-);by8Ye-q*TQI~ynaMrC(cfgJ8$&0pxUAEbx;z3h*Q=X>h>YmU{R{uNW7=qv|ExT)`QFZwVfj1`bQ;bcGchI*)0p$)|vP@z?YN7yQ~&f!~}PRlE$w>$Y1x*OHbgf=)6me2Ek zg$61UzX7HaPdTTdv0Z3vTj+VQ?sslMS}p1kHOv1B#2933T{T`tW-HB?bj>!y^|5zyRf=mraUH;}wgKYFYvK~?Y&1F2^%YVd;~FBV zkJ9*r9HsHe`6zLZXRGeMq^$+1EVQ<$--U6RQ!^baT`lUt+~qVLqiD5f2B9~bU7alN z`JZ$Rx7>+G!U}D-tcHDxcqE2~pK*Okd}uzUwIx>jr^JUQp}iIU0>A1#Mt{OPE!*h` z@M*!_K#iWGV=f1NQ8@v8iT>*H(QD%QhWNZK_#S&^=tFUB6W0@Pb&yBB2K1`WfdO?0 zm{3Q6?bRt@nfl7*qF(AOV zez1BGh`l&}H|j}#;Q3Sl%=D*@DDX=za3rus!u6*~G|qpC1g)h8$aSKhB>G0tG>K-Z zXqw@f3~Uzt7STT-`hB8l70rG;f+hp|C1#swj?vN#RL4Z66!*iT*vX=}Hwn=sMUxaw zjcEFdroU+FL^DY=lSI=bnx&#yDw<{>l2hD`EnZmxiiRPGS zNaJcsljJn6hF3BWE+HDcYJfy*MD8!vTJf(Fd6Kr2p7T!*hvY-1rvhRg0+H;g3UHgbgOLxbqX91xmg#D&YFGt?NsYO zAezng+sW{^+1aWx4lb;MaR)b+5V@UTwPQS$Sk?dp6LY{M8=J|L6QQqN?@qWT_LmNz7YBZWTQ0;S}3MPuP~jLBZ}#+7lN! zA#%0IHC_%~D{`Ia8%1ssd9%n{MBWFP=f9AnVrUbYe9{1(^pE@FA}4&@ShdKtf{lWk z1@{TIiqBDz$uAN7lA_27k*h_n@o&H@7_}^C{B>ez6ho89%|M*5qS+^SRFDD^G{Ey& z1$aJN1MC?WeF8F46uCy^T9F$Cn?%zr{`*AJiU_azGe?EAiJ5}jfHf#VMNS0SsaoV3 z!CJvO!A8L*!OenOP&e&qMXi(mHqjiHXez{!6M{8@bs_d_%1E(Akitu$iVIc?)(SQX zZWi1pcvO%g;xAY&SS#2lxLI(Y;88(}ioalOl-GvM(UDy2i*j~H1u2K+xL~zltsv!! zUa(rQR*>>IXtkii<_o`azx(P=!SnP-^*`&S_6zJ&?R^~=I_4N(T-`l$zax(QlB;~} zfy;_m{-A*67YaH9zbx(!To9{dIll_{ae{qj#n@+K0n3+)d`kSg#n}InBKH5Rko||n zE(AVZ*dN%>HxQT?8v-1WxPtYWD3j*mQNYK%B1hSJUZwS`lF8};eHMIzst z&-zPatUp!2_+=4eO@MvY6>u|i1&0YP*JnX9L2#$wt@-S8X)MFdoGM_f39!?bMJ#W# z&jG$AxFo@@$D)gXC;gn<$ev|IY+Wruo6kx83F*Lw&~jK);(4?EcF5NY7CMIb zG(7ZMj7?(>W=2zQY=VrzGrdM*>3rZgx&SyH?{;bUZK4KvE%gUZz&k1$)zW3asrW+- zMb`l}Jn0XHJRPXvIzANg44_6c@mD>(-#rX+8m~kvx&f%+F?}TDIY5mXGKAl0Xf9C0 zyDL{i{v{B5SV#%KVgoh02`MSM8K~jj;2Ovaff_ABN{SW(HSC<$L2d+U*mRf#c?nR% zO~z!%c=(3Aj9;;#`(o<(|m=a(TrjZ&R}SE(-8TNw>GKw}|S zq0?#O*tju$N9Zf`4f;RyzV_kv*X(cG|6;d03LQyD7e^0AzO$>d+Bws?*!i*ZYp2&$ z>FVpMam{pPT_XoY=Wu;+wPn9<(>&08AT)eT$TeVN* z@VkaOZOXAo*(KL1X$~VhsqdlzQr|_CYvY$aao&-4KI|3jQ6hTb>37pSD%LODiyV_QZf5$6mU#aIgU5fB*zEJ?mPqA2P(() zfg0#~kLK!wH3Kn?zSnLEPMR=bz_f-NX4Ku(e@0zHLq%`nool-o$q5~;m3=0Z8?VWuf;Vt)|{^=PrCQq6>Ej?>T zpW6Li>($pyp0netnJXO5xl-}cX)|_Ye)&(Ef9mA969!Mdse$^ZXHBY`yW^J&W6JSe z3p-XXdRVjFFlX|VX*z6w^XhTM zr4@z7a;T}3aq9N-R4=2;%I)d`|D;u@Q7Kft^K_Dr_b|JZ9ypR!U8su=9RghLu z$ZiY$Bm{<1L$fx#ni&lZ*L(zM$TQQt@A=n6^7rq zFy|&4rjF0&h-<|@OR#Agk4NJp##E!7^|oNsD&tNgrS3NH=Ti8~$>f^u_&dpwYc4M2 zyf*pc7Uc^yZZh-OBqQ{Q@#LDp&T2kQ@rM)2u-<(`NP6MOj$^3Yq6bGC%kF+l1&wiQ z{|m1(DTmJ(w60H5+}s%4?>olb!2w44tmWLduB&qe9cZ}7#WTr&&$and&6)bn)sn&y zN)cO^-Lu%~!ml6$CQqC<)$re&pX$}CzJ6jwpNTzsPVU{azI<|hkBTW3Jtp;@G`X&R zVr6-KpWc0XfYupT-21I@#rlA6^B(R<_OoY?F>8H+_UN7+E7s3YJA4}tX~TL^FRI7C ziP%~11MERPajEJJtfz8F_0Uw%6nuN&a@HH)$+*O^en8b$H4CJOB0D(I@avLYr5yRwV7Rd-?K^1XGYLDc>K=lP%i8<@Aw zd(Np-r>d)~yK6eh-z&-O%00K|`~T!ULB^+x@*`=YoM;^sYD)R+i*3KY++Ft~(Zy2H zL3E?3E$Vl65jnwYV93#|hbY~z<9~=mXwfZH2n0|4jwh`;sM{$j>o#M8Q-InlKRHR3gZLN)%h{zGQp{B}u9 z@uy^Aj{im$u_Qt|G>WyLj6bVi9J(9TKMA#nt?l($_X+hCc$HnQN5f$~C#OQZpkI_z zC+hX5bNY&x^-pugi5K;Oxt{qH} z9K+G<_lrSxPE$Am-6OidH2uWOAiuQ&xqd^jatf!02C)i4uPeu}f`=Db|2|dYw-E zGG)#Pq)=9?%&8#CE*tUFj-;RNh$&t|Hu};~| ztc7w=4dPlT2Q)=W6}1;TD)HCh$U~WCErZr-3}cPaI!Ca_U)OUB@(QHy>15sj*>5V^ zPew-1aYp(z1y$7{WE0y2dr@lEZHMScpPtsd1A!dD+@K>1%06} zExuIRJuvw^@i$@ec?#pVNuAf{%@`lQ9imiW9;k>R4?OcbW$7FrUoJ~q#(2NYXY*O_ zY8+O4@?_#H5qleuD79-ApBCEZcYOYYKQ{yqOheVeZA+q{AHMgHQ9DC68SLjNihhA*W6AB==e3 zi3KdEaU_CKY2>l8(F@TSN@;1{d#d5TNP))y zBSTp@4dEh{@@n(ZDX)F(h3Q^yqEP~+etz+#13!i~J-#34)RfpKDAP0%>q1#JOi3?v zESQDyQLGDQ%VIu`y;&1VOw->h?kPUie<)6*e+J+EekJcj%9VX$2lRV-ReKM@gu|0v zZl+mQc@NRuE7vdE@>_7W$i{FInqytMuOuS;df$>5Vooe6Py7ugr!qf@=H`74*$@vw zjA6E@iZTy#%iR2TX`44!isK+Q{{yf9Gs#S9#)OP>uT+R)I3FC%hXF2S-mNf|UY9E4 zUqI(qONQVK{8EZovNVU57W<0M#uRb*t-dtBO_?Xd#lMDwT-@T{KqzzAS^g(vRQ^_~ zUf!>%>3v*SO45huqe=%PzJsMj8T)TZJSFv5%#KJAi`c9yaEZ*0 z9|bKv-{#Y1Xjp;TxC4r%j0efHJgV5-u}^?~F%cv13rgk4smd&lfu(1^0eu z(SH4SX<6c=G?I{G#Jvy@;|gF_qr57cAQk;rlh4T=EwY~)q`7AfQkv$#AR(bNEOZWk zAQi$N^abOxg|d(@xbR%qCvw-D&CnJ4)BkHCKL3Bu!sqt|QYjb%eAav{a%YaQxp#rE z=&{}rn~L})mY7w)q<41629h4P1N|W_6mv)+Q_GB{>8-sZy@EIdA3tWo-RV4BR?PlP zWxgy=1aT?NR)U!BSk26Ecj~))XNb@BzxFPub|Jw^-^u z(5J_}vc)_tFXod%SPSE1xu8!(-tv@u{3#}+G7k%k*&kNs%ko4J_Y7w=9gg{BJx7U^ zhtMx4VpeJn2c#j}Xg3HFPnViVNr5jC&yZr&m#sx(nGjKgP{cCioXdiw*Ouqj{Fwd~ zXQa<5i36xEC0+``UT~E6MRmlMk5=+m3eY_VrSvZEW5~vEcIp8dF2_+IgtDX zTEVQvm~^pRFs;C-k%{!Qf><7Om_?kP9PQ?agec(FGx97fGZzI)(qoD^7k$r%HKnn{ z3RqT)c*uNN@gCs*oLL9?yeb4KOZVmE&5IXFF%9Cxc#(c~MO0+#brnyEp1P;+IU?3R zwr{o&#d=e}(c*di<$lwPwxL6@UZ~Wock^!{Cv|kQS6GQ5Z%41NiElc3=?#^IXJ=xa zD_BU>it<=Kuu6u#0Xzc%Bmp+utiJcT(WKPs*E zEeok6G>KPxcVv2KXGrSb@%|y9sr0#TO+cj@M!^_n+{*ZXS6Ul5#5rCW;)3uVze*7V zQfV>cA;y_(-2+bton`u1RcTLnS;$Q%@(u?z`jdZymQLlSha)Pz-g2tjS`Ga?oqJvdtrX*1MO5?xP}fSfxNv*1bDpS?E$)&zje@ zKSxzMPx)AN)7kFBK{p+WNbCh!rTt=A=(U7V`DG#V{S1{=IthbHkGVErZ1MsUA3={b zI>@p!u*dJ9aR}<52f5bsK{-Od4vflC>Hd5vFLsa0anP0UQE7Rm)Zguv$^Ax1d=IjN z3fY?Ynj6HrgnKKUuvuJi`dDujjSFq=+NHL54?vm*aWQ{#2;|$dPA!jS? zw@4IndI~RniCJBBnthG=TSX>H;0^&sGgjv+vJ>v_z}ir;3bQ?f1eh;xhgwB`Dxemy zWJIz|>dm$sw)LcHwq56zwq7_dr012)`qEf5>ZK1erELUFW41ml**IE~kO|V?xWVys zH<_Yd+O5jwCrE}cGm+W~nnjN@D@2c0&>VV#Sv$`8D!PNVoKbHDJ;$~e*>*R*aE48% zR|#u4J%Zk^pjFgCi73snokLN&({>(kq+QB?VflBq$01*3-vWHu{tWOz+w;KP?DLpi z`ajQgm$3XL``^U=eb_%}m;Nj5PA(C3@L9l;uwUW>G;myZ?#G?HNA+ z{~0x{A@widtvJd;!rasE&Qc+qS{F$m56q?nE#H#ZI@xVl5+WOmdF4 z)iEZScab}(*aJZ{fP6ZA1-oR z+SIgSXbw8&3PEYEgjAKY>u8w!iB`35+5mdgG9WFTZl<$<7Ym8^vpk38cBKZIPgyg7 zE`nSpB(^D61Mf6RSusnz+$=S(v)s!fHF1m7Ji)S{ixMDqAywCM0}q(@0Y_W9fwCDve^dSnoXDEDEpp6WHGQ3S z=7fsUd6-smBqDN+T56kWtHmwy zc!TA*!xlkxDVF(2W)<#c=Q7(#YqNK$CfZ@J4%b+VnT{FkpmrNB%+sTU&r(|`r&h7h zEQ2Mo!WIkNYOpC;c}@#0!uiLpD|35*-eDMfhl|1PHQ3{!-eBue49Wb*WTAJMrIJab zRxC9{J4q(vq)0)E$z+^V-b1pbwi}|gii-vr4BL$s7Y*-0vi+spLl=!_V=AbJ9yXW^ z>ZN@KlR>?7fm{X=^s`W{;-j$!`#Lvl@zIqA+khVWXtKe&(Lo>|>TnG(^8IlF6icpG7NsrkG4Li~bdpY^m)n zbRwHh80;h0P-iw-i(yK@_-bx}C5Jqcg8d=4#+gHz274oN8|6~OV0$7LI&-PmVAF#I zmM~QqY)-Jo8K$!g=E)uF%%gJ*R*-uY*o7&EPT=l|W-v>%(PMdy&Is*e+C~SMjm8Zn zQ5)@K_KLyAGwV^3ve_fD-b#Zd*=8}<)D=3D1Vpp0=nH`T@T!!j z>~AftXV#V}yIV^y8@3&u#9u+*G3b);Vduq^#1oaw?Y{80&N1{I_Soc1cq4y>^HQ2U zBxReO@muFOLPzjfZTmiQ6fDzVzU<>*Jq$Kk6|QkqZLo5-jWSqi=x742ml^cyTv@T+ zU>mX|yTM@Vb0oXPVBdRW&=;At(MX$2=CQM7GHsO9EU?WTmSV35(_EKRWO$056ZW~V zG&5UGhca_qiHY>ixoqU2nMh?dk}b7WWsS8=r2YoG08g_M>1=}qVY`aXGgwd9uA++# z_El)CgA7#iq1D?a{H8>qs+LC@SbW$~IVA@Elho6&UQH;ApVY6yuX?It?>yBR#j# zbQ)=}OFUk%OAIyuEnH9I4K@NTTu=2QjO*XG7>pUzXc(33BG(L>Ww3X%$69WnTMgEQ zEruItvB6Xvm?qkgVw_A9JJuInr5-v*QGI`&D%$S_N7z4JTGtH@@sVX&IWgV?&mTG<-J$l+x#U{_w%obDPO?u6>4*d~?-C6v89&t-~s@%rmkD#G=m{#cC zEW+dRO+Yhk$tC>G-Zw0t;J1W+?!O54h6)wJ`qMaoGI_+=6&2rX~q42AHUbD;DN#G zHQ`ag>ow!C!Rxi)fx+vQj|yI|6^{*GuMH0vGU)%S|1VR23Y!iJReLZJ3ECHwL;3<# z7Fr9HptVRsp?V(JwYarbXaamp6oH(wVvP?;pB+3-DgT$T<#ft=cDA%$U_?+5A(ccb z+K;lJgd_@`jL6C07}g-hy8y=llZ?JS3z&8GP##Ix&I&8BzH3TBS^|W zBCr3_@}ugM{NF32w5NQ2S?f&Ae~VDUhd6~8@5IjrR`&ALfjESd#6%a%9V&WD^HF_52#WI=%8Ytmns;m7^@l2XB@*gfw3Nl6CJ4H67)!BkP^$7v;nheE#teu z2z3LC(A*=Lr4;4G6&u%BRGqwE;NNzJ#@-|Z?Z!=Z$Hq#IeG=u}yuw29PXqHE_JdWjYEDzyk zY8Xc|j$@q9+g0;PZdbMPc2z6M?W$Ii+f}WUko#4wB=@UYN$yv*@_tn-?^iA729|RJ zD_LI2@^TKnlCfAhgzbeVc_gY7IUZHY*uY}ZDPGFDLsTjI+-<<8q7o|SSRK8z~QOtqO;u-oCT&qRM`xJ0|^f_RQzf(*=XfI7rZivcZwE5o?&!N@< zQLL=Zeowpt`Iy*FF0WmAi{YlKdZl3*EUUvyja+Dr3U6rbZ>6KiA}BO@J|n zu?O%!rcz*)c^Qw#GNsVAMzksSYSrfLG&@73_ZhdL#rX-S9?4u}ejli!X9xVUe+5wrJ8`x6Brk zqOV(?QTB)5wsdo}DPl$b7nYFnS@c^=D52~P9EH>Js70NRd~1~{+?!3xyVj)$)TDg@ z>uTJ;OcA@W=PtvA)fwE^DMAkBY{nUkGQ~dX6mI5mWr1se+9|R_L)2}`;J_H*2cav} zL!!+yN!>+z@}~e3U**q0@exQX`lQk~7{I$!p{$LpgyvoA8lW7HHf3kl1Mr;b*`=;z z&%I(#{$X{mxJvsMG;;8#hjt)N=3y^Zc7m{t`-|h;ALAKYtoU$d7c0lLv2guT z>tjEvT$A75F3*c$sP>vR!tOIYk~!L5FZM((1HKWt%HD{^Cfg4wP1<#Ay}`Z-X|;*R z#URfd$icuod(?D9ZLyn`m%?l9PGv~u7JEREV~_|aa&ed`-g3QdpJ{|stnb-#*tG|{ zma=Opd&-q=k$EPXJdP&UWSSh6m@Bo)5JjG5Llk++JxQ|1GsRKtrnDiI`Q|ophaVR& zG0--^(Iy%*ZlfBE;u^Iof!%8TRj942{a4%5)Q zHR@rKhbE+~!7Op(caIjlZSoA=4t$<*8&JWo94j0)>ZH4YJ7}fDO>eX3yX^A;<6ibm ze9W%h?0OKcHgbxafiAHY=oRyUQE@x4P&@!E6Aw8Y)L%RXxk@~Po!#qcx~GulQOMa7 zdjy|*dgGU)0q)-T#b_8XnT3EA~HLdV`)3cHFn^^x4>pNJ}$(lFtr1hrf4UXB(n$Ms~yy^Lj zRYJ&qn1pmP3E7({YYJFXz?y2-oXeVXSyRuN2G%sNriC?&S+kfmtw1CvWH&ak^&ug< z*C90tPbaJ1U{x18b+hI(){r8r35AnWWHr1HfpAgQ;1_))TFvsgY^`JedX^iM#Wcm! zz&=UVx3GRO<3`pbHZkd7(#di+qcF`wmjGGuc8M{m}(QcOUVvtPK ztkXll_4KMI!kXcXb=Y=MJW0ll2`)MqyBNC}$s(NuV}vovIGnMLG0E6!k%?}$tfzN8 z+gWZ^S))o#hk7SjJ=|IctFTEl+Y+*f*kof-R`p;U&YEhL>sU@QZe-lV z*umJz*v%;H9EY)*v7WJoaT8-FV>hF4G$iCeIb;%1hpb%9Si_onmRlG%F?KR`Gn&)Z z(>_l$jb{MlW=}n13*#onPPTTlES%Ca;*@hD>Re9?J;Nan8v!gACt8yn7Y9b=MlBVz|+C;N18qMi*?fNy$| ztZHG+MwUAmyBH6!4|zGXSI%wW<+;t8sFzb@xtirVmZv~|)6>G5jjZWl>_pvWPgjCT zH=7TzEBRyt5yq%bIt^#Jnz4?to-xVT!nnyNo9~4GeV*-*-}H2|UT88}l(CwzUXz|J zj9pq{F$no3Mi_@P)-fg-H!^lGb}>?b{TYWd)-fg-H!^lGb}>>=`VSAvRUyf^F(}jO z2sR8y(Zz()x!;V#8M_!MgY}HV8M`uMIDtJA`F8!k#}|8stIg_G^?S zWdCE?(tmTd^jE{50cT`?E@k%@z&*L&0KbTQ54ZTFBP}6`FxRnvomP7ej8sj-;SCP@x;? zQphub3e8H;WgyK!g=W(gkmmpux(Ue${JK8@@?0b%=oX+tw;~yVyMe1A&qp$X7627m zh@TX&RRmNhi5m-n%>ztIT0~PJF9s^K1PKdT3RLJeBrNE5N;HBjL&gH{s9X>EPGl@- zIZ(m-Cvo5wO0YX}Ib<)5hg^kD$Mu4%CnRoDcdG~0dTXO~zIDB|)4I$0uJuFfYqmey{%Xsx z$LwwP&Gr}U`|Stp|FCB`iXGLC7hfbCGjmezyPWy?6uWTPBR&0@>Ntwy{&U?Ze)s6K zpWI-Y>Nt$5KX=@~d#tA&+3(@VFa5@1lkewst-PUiX5A)7nA>n;6V-#S7hfOQNl#94 zCoqxmF_RzfZpt#o)m8e4xk}CH3iK+~r{iTKlwYNrxja9^!sTG2xrun;Q>HQyZ+PNm zM7o6QJ19Lv)_2fSuJ0gQhOF;+hS7DBLllxC|BzR*Yq!4ARbp2h-cNe zX|wfV@rv~gVYj_4w%EQ9e*4>^1K)qz_mbZ}R2k~f@49A_KBBJ3TsiQXa{b+gfT+|b z*Olu#>%M85cHw_<>((EqXG8BwL*f7cC>p;515LR(cLEK^7o z$t6md!UDXyz1Q940)Ol^@t(6qZTq@=nuNoZMvAY+h5x#CZ{;KlDK3}I6u~x+)taN^ zYRT_y_-a$NsTgvtgsBI(F)esrAiM@z;_d6}`*lKcaqudNV1R)_+>R zf&B&ys%V%pU~1){hJKU#4xBu&qOyES<$#Kc{tXp_rVZ*lsD7Z{xc;Pm?FOHAJJ!8Z zpI=Ei(iTluR=(K&s|_=RK3rtByC3+Ya`80kk4scPT-y5402+ji=LVVrX)3mH2SM(K ypS1eIIvL*z=*nTOguMd)`ooSF^H6^fd Date: Wed, 3 Jun 2026 12:12:23 +0800 Subject: [PATCH 110/202] Pluralize standard name and continue on error Replace premature 'return' with 'continue' to avoid exiting the processing loop on individual template errors. Rename occurrences of the standard identifier from 'DeployContactTemplate' to 'DeployContactTemplates' (Write-StandardsAlert calls and Set-CIPPStandardsCompareField field name) so comparison and alerting use the pluralized key consistently. --- .../Invoke-CIPPStandardDeployContactTemplates.ps1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 index 3290eeacc133..90af07f8d648 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 @@ -150,7 +150,7 @@ function Invoke-CIPPStandardDeployContactTemplates { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message $Message = "Failed to process template $TemplateGUID, Error: $ErrorMessage" Write-LogMessage -API $APIName -tenant $tenant -message $Message -sev 'Error' - return $Message + continue } } @@ -356,10 +356,10 @@ function Invoke-CIPPStandardDeployContactTemplates { foreach ($Contact in $CompareList) { if ($Contact.missing) { $CurrentInfo = $Contact.Template | Select-Object -Property displayName, email, missing - Write-StandardsAlert -message "Mail contact $($Contact.Template.displayName) from template $($Contact.TemplateGUID) is missing." -object $CurrentInfo -tenant $Tenant -standardName 'DeployContactTemplate' + Write-StandardsAlert -message "Mail contact $($Contact.Template.displayName) from template $($Contact.TemplateGUID) is missing." -object $CurrentInfo -tenant $Tenant -standardName 'DeployContactTemplates' } else { $CurrentInfo = $CurrentContacts | Where-Object -Property DisplayName -EQ $Contact.Template.displayName | Select-Object -Property DisplayName, ExternalEmailAddress, FirstName, LastName - Write-StandardsAlert -message "Mail contact $($Contact.Template.displayName) from template $($Contact.TemplateGUID) will be updated to match template." -object $CurrentInfo -tenant $Tenant -standardName 'DeployContactTemplate' + Write-StandardsAlert -message "Mail contact $($Contact.Template.displayName) from template $($Contact.TemplateGUID) will be updated to match template." -object $CurrentInfo -tenant $Tenant -standardName 'DeployContactTemplates' } } Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: $MissingContacts missing, $ExistingContacts to update" -sev Info @@ -384,7 +384,7 @@ function Invoke-CIPPStandardDeployContactTemplates { } } } - Set-CIPPStandardsCompareField -FieldName 'standards.DeployContactTemplate' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.DeployContactTemplates' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant } } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message From 058da5e0d615c944b4cc6a11878a1d4602c3397d Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 3 Jun 2026 12:51:53 +0800 Subject: [PATCH 111/202] rework mail contact standard and lazy load modules when needed --- .../CIPPCore/Public/Add-CIPPScheduledTask.ps1 | 20 ++++ ...oke-CIPPStandardDeployContactTemplates.ps1 | 94 ++++++++++++++++--- 2 files changed, 99 insertions(+), 15 deletions(-) diff --git a/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 b/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 index 2051ccffce9d..94142b32371c 100644 --- a/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 @@ -64,7 +64,27 @@ function Add-CIPPScheduledTask { $RequestedCommand = $task.Command.value ?? $task.Command + # Validate the command exists — on HttpOnly workers sibling modules aren't loaded, + # so import them temporarily for validation (actual execution runs on activity workers) $Command = Get-Command $RequestedCommand -ErrorAction SilentlyContinue + $ImportedModules = [System.Collections.Generic.List[string]]::new() + if (-not $Command) { + try { + foreach ($SiblingModule in @('CIPPStandards', 'CIPPAlerts', 'CIPPTests', 'CIPPDB')) { + if (-not (Get-Module -Name $SiblingModule)) { + Import-Module $SiblingModule -ErrorAction SilentlyContinue + if (Get-Module -Name $SiblingModule) { + $ImportedModules.Add($SiblingModule) + } + } + } + $Command = Get-Command $RequestedCommand -ErrorAction SilentlyContinue + } finally { + foreach ($Imported in $ImportedModules) { + Remove-Module $Imported -ErrorAction SilentlyContinue + } + } + } if (!$Command) { Write-LogMessage -headers $Headers -API 'ScheduledTask' -message "Blocked attempt to schedule non-existent command: $RequestedCommand" -Sev 'Warning' diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 index 90af07f8d648..b6813487ee95 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 @@ -127,16 +127,73 @@ function Invoke-CIPPStandardDeployContactTemplates { # Check if the contact already exists (using DisplayName as key) $ExistingContact = $CurrentContacts | Where-Object { $_.DisplayName -eq $Template.displayName } - # If the contact exists, we'll overwrite it; if not, we'll create it if ($ExistingContact) { - $StateIsCorrect = $false # Always update existing contacts to match template - $Action = 'Update' + # Fetch extended properties (Company/City/Phone/etc. live on Get-Contact, not Get-MailContact) + $ExtendedContact = $null + try { + $ExtendedContact = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-Contact' -cmdParams @{ Identity = $ExistingContact.Identity } -UseSystemMailbox $true -ErrorAction Stop + } catch { + Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: Failed to fetch extended contact properties for $($Template.displayName): $($_.Exception.Message)" -sev Warning + } + + # Map template field => current value (null/empty when current source missing) + # ExternalEmailAddress comes back as "SMTP:foo@bar.com" — strip the prefix for comparison. + $CurrentEmail = $ExistingContact.ExternalEmailAddress -replace '^SMTP:', '' -replace '^smtp:', '' + $FieldMap = @( + @{ Template = 'email'; Current = $CurrentEmail } + @{ Template = 'firstName'; Current = $ExtendedContact.FirstName } + @{ Template = 'lastName'; Current = $ExtendedContact.LastName } + @{ Template = 'mailTip'; Current = $ExistingContact.MailTip } + @{ Template = 'hidefromGAL'; Current = $ExistingContact.HiddenFromAddressListsEnabled; IsBool = $true } + @{ Template = 'companyName'; Current = $ExtendedContact.Company } + @{ Template = 'state'; Current = $ExtendedContact.StateOrProvince } + @{ Template = 'streetAddress'; Current = $ExtendedContact.Office } + @{ Template = 'businessPhone'; Current = $ExtendedContact.Phone } + @{ Template = 'website'; Current = $ExtendedContact.WebPage } + @{ Template = 'jobTitle'; Current = $ExtendedContact.Title } + @{ Template = 'city'; Current = $ExtendedContact.City } + @{ Template = 'postalCode'; Current = $ExtendedContact.PostalCode } + @{ Template = 'country'; Current = $ExtendedContact.CountryOrRegion } + @{ Template = 'mobilePhone'; Current = $ExtendedContact.MobilePhone } + ) + + $Differences = [System.Collections.Generic.List[string]]::new() + foreach ($Field in $FieldMap) { + $TemplateValue = $Template.($Field.Template) + $CurrentValue = $Field.Current + + if ($Field.IsBool) { + if ([bool]$TemplateValue -ne [bool]$CurrentValue) { + $Differences.Add($Field.Template) + } + continue + } + + # Only compare if template specifies a value; empty template fields are not enforced. + if ([string]::IsNullOrWhiteSpace($TemplateValue)) { continue } + + # Case-insensitive compare for email; exact for everything else. + $IsEmail = $Field.Template -eq 'email' + $Mismatch = if ($IsEmail) { + [string]::IsNullOrWhiteSpace($CurrentValue) -or -not $TemplateValue.Equals($CurrentValue, [System.StringComparison]::OrdinalIgnoreCase) + } else { + [string]::IsNullOrWhiteSpace($CurrentValue) -or $TemplateValue -ne $CurrentValue + } + + if ($Mismatch) { + $Differences.Add($Field.Template) + } + } + + $StateIsCorrect = $Differences.Count -eq 0 + $Action = if ($StateIsCorrect) { 'None' } else { 'Update' } $Missing = $false } else { # Contact doesn't exist, needs to be created $StateIsCorrect = $false $Action = 'Create' $Missing = $true + $Differences = $null } [PSCustomObject]@{ @@ -145,6 +202,7 @@ function Invoke-CIPPStandardDeployContactTemplates { Action = $Action Template = $Template TemplateGUID = $TemplateGUID + Differences = $Differences } } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message @@ -350,19 +408,20 @@ function Invoke-CIPPStandardDeployContactTemplates { if ($AlertEnabled) { $MissingContacts = ($CompareList | Where-Object { $_.missing }).Count - $ExistingContacts = ($CompareList | Where-Object { -not $_.missing }).Count + $ContactsNeedingUpdate = ($CompareList | Where-Object { -not $_.missing -and -not $_.StateIsCorrect }).Count - if ($MissingContacts -gt 0 -or $ExistingContacts -gt 0) { - foreach ($Contact in $CompareList) { + if ($MissingContacts -gt 0 -or $ContactsNeedingUpdate -gt 0) { + foreach ($Contact in $CompareList | Where-Object { -not $_.StateIsCorrect }) { if ($Contact.missing) { $CurrentInfo = $Contact.Template | Select-Object -Property displayName, email, missing Write-StandardsAlert -message "Mail contact $($Contact.Template.displayName) from template $($Contact.TemplateGUID) is missing." -object $CurrentInfo -tenant $Tenant -standardName 'DeployContactTemplates' } else { $CurrentInfo = $CurrentContacts | Where-Object -Property DisplayName -EQ $Contact.Template.displayName | Select-Object -Property DisplayName, ExternalEmailAddress, FirstName, LastName - Write-StandardsAlert -message "Mail contact $($Contact.Template.displayName) from template $($Contact.TemplateGUID) will be updated to match template." -object $CurrentInfo -tenant $Tenant -standardName 'DeployContactTemplates' + $DiffList = ($Contact.Differences -join ', ') + Write-StandardsAlert -message "Mail contact $($Contact.Template.displayName) from template $($Contact.TemplateGUID) does not match template (differences: $DiffList)." -object $CurrentInfo -tenant $Tenant -standardName 'DeployContactTemplates' } } - Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: $MissingContacts missing, $ExistingContacts to update" -sev Info + Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: $MissingContacts missing, $ContactsNeedingUpdate to update" -sev Info } else { Write-LogMessage -API $APIName -tenant $Tenant -message 'DeployContactTemplate: No contacts need processing' -sev Info } @@ -372,16 +431,21 @@ function Invoke-CIPPStandardDeployContactTemplates { $ExpectedValue = [PSCustomObject]@{ state = 'Correctly configured' } - $CurrentValue = if ($CompareList.StateIsCorrect -eq $true) { + $AllCorrect = -not ($CompareList | Where-Object { -not $_.StateIsCorrect }) + $CurrentValue = if ($AllCorrect) { [PSCustomObject]@{ state = 'Correctly configured' } } else { [PSCustomObject]@{ - MissingContacts = $CompareList | Where-Object { $_.missing } | ForEach-Object { - $_.Template | Select-Object -Property displayName, Email - } - ContactsToUpdate = $CompareList | Where-Object { -not $_.missing } | ForEach-Object { - $_.Template | Select-Object -Property displayName, Email - } + MissingContacts = @($CompareList | Where-Object { $_.missing } | ForEach-Object { + $_.Template | Select-Object -Property displayName, Email + }) + ContactsToUpdate = @($CompareList | Where-Object { -not $_.missing -and -not $_.StateIsCorrect } | ForEach-Object { + [PSCustomObject]@{ + displayName = $_.Template.displayName + Email = $_.Template.email + Differences = $_.Differences + } + }) } } Set-CIPPStandardsCompareField -FieldName 'standards.DeployContactTemplates' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant From 2edc759287485c1e4cec76e345a04323457a63c3 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 3 Jun 2026 14:11:33 +0800 Subject: [PATCH 112/202] Handle PendingAcceptance guests and update reporting Include guests with externalUserState 'PendingAcceptance' (unredeemed invites) in selection and treat them as stale. Add createdDateTime to filtered results, record reason when disabling (either unredeemed invite or last sign-in), and compute separate Pending vs Stale counts for alerts and logs. Extend report payload and expected values with GuestsStaleSignInCount, GuestsPendingAcceptanceCount and detailed pending-invite lists. Minor logging/message wording improvements for clearer diagnostics. --- .../Invoke-CIPPStandardDisableGuests.ps1 | 43 +++++++++++++------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableGuests.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableGuests.ps1 index aaecff145875..d5bd521a5a58 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableGuests.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableGuests.ps1 @@ -61,6 +61,9 @@ function Invoke-CIPPStandardDisableGuests { if ($lastSignIn.ToUniversalTime() -le $Days) { $guest } + } elseif ($guest.externalUserState -eq 'PendingAcceptance') { + # Never accepted the invite; createdDateTime is already <= $Days due to the server-side filter + $guest } } $GraphRequest = @($EnrichedGuests) @@ -104,7 +107,12 @@ function Invoke-CIPPStandardDisableGuests { if ($result.status -eq 200 -or $result.status -eq 204) { $guest.accountEnabled = $false - Write-LogMessage -API 'Standards' -tenant $tenant -message "Disabled guest $($guest.UserPrincipalName) ($($guest.id)). Last sign-in: $lastSignIn" -sev Info + $reason = if ($guest.externalUserState -eq 'PendingAcceptance') { + "unredeemed invite created $($guest.createdDateTime)" + } else { + "last sign-in: $lastSignIn" + } + Write-LogMessage -API 'Standards' -tenant $tenant -message "Disabled guest $($guest.UserPrincipalName) ($($guest.id)). Reason: $reason" -sev Info } else { $errorMsg = if ($result.body.error.message) { $result.body.error.message } else { "Unknown error (Status: $($result.status))" } Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to disable guest $($guest.UserPrincipalName) ($($guest.id)): $errorMsg" -sev Error @@ -121,26 +129,37 @@ function Invoke-CIPPStandardDisableGuests { if ($Settings.alert -eq $true) { if ($GraphRequest.Count -gt 0) { - $Filtered = $GraphRequest | Select-Object -Property UserPrincipalName, id, signInActivity, mail, userType, accountEnabled, externalUserState - Write-StandardsAlert -message "Guests accounts with a login longer than 90 days ago: $($GraphRequest.count)" -object $Filtered -tenant $tenant -standardName 'DisableGuests' -standardId $Settings.standardId - Write-LogMessage -API 'Standards' -tenant $tenant -message "Guests accounts with a login longer than $checkDays days ago: $($GraphRequest.count)" -sev Info + $Filtered = $GraphRequest | Select-Object -Property UserPrincipalName, id, signInActivity, mail, userType, accountEnabled, externalUserState, createdDateTime + $PendingCount = @($Filtered | Where-Object { $_.externalUserState -eq 'PendingAcceptance' }).Count + $StaleCount = $Filtered.Count - $PendingCount + $AlertMessage = "Stale guest accounts found: $($GraphRequest.Count) total ($StaleCount inactive >$checkDays days, $PendingCount unredeemed invites >$checkDays days old)" + Write-StandardsAlert -message $AlertMessage -object $Filtered -tenant $tenant -standardName 'DisableGuests' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $tenant -message $AlertMessage -sev Info } else { - Write-LogMessage -API 'Standards' -tenant $tenant -message "No guests accounts with a login longer than $checkDays days ago." -sev Info + Write-LogMessage -API 'Standards' -tenant $tenant -message "No stale guest accounts found (threshold: $checkDays days)." -sev Info } } if ($Settings.report -eq $true) { - $Filtered = $GraphRequest | Where-Object { $_.accountEnabled } | Select-Object -Property UserPrincipalName, id, signInActivity, EnrichedLastSignInDateTime, mail, userType, accountEnabled + $Filtered = $GraphRequest | Where-Object { $_.accountEnabled } | Select-Object -Property UserPrincipalName, id, signInActivity, EnrichedLastSignInDateTime, mail, userType, accountEnabled, externalUserState, createdDateTime + $PendingInvites = @($Filtered | Where-Object { $_.externalUserState -eq 'PendingAcceptance' }) + $StaleSignIns = @($Filtered | Where-Object { $_.externalUserState -ne 'PendingAcceptance' }) $CurrentValue = [PSCustomObject]@{ - GuestsDisabledAfterDays = $checkDays - GuestsDisabledAccountCount = $Filtered.Count - GuestsDisabledAccountDetails = @($Filtered) + GuestsDisabledAfterDays = $checkDays + GuestsDisabledAccountCount = $Filtered.Count + GuestsStaleSignInCount = $StaleSignIns.Count + GuestsPendingAcceptanceCount = $PendingInvites.Count + GuestsDisabledAccountDetails = @($Filtered) + GuestsPendingAcceptanceDetails = $PendingInvites } $ExpectedValue = [PSCustomObject]@{ - GuestsDisabledAfterDays = $checkDays - GuestsDisabledAccountCount = 0 - GuestsDisabledAccountDetails = @() + GuestsDisabledAfterDays = $checkDays + GuestsDisabledAccountCount = 0 + GuestsStaleSignInCount = 0 + GuestsPendingAcceptanceCount = 0 + GuestsDisabledAccountDetails = @() + GuestsPendingAcceptanceDetails = @() } Set-CIPPStandardsCompareField -FieldName 'standards.DisableGuests' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant From 64efbd97477f8fe0ce907f7534c253d6b7239d01 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 3 Jun 2026 14:38:32 +0800 Subject: [PATCH 113/202] Apply multiple fixes to the add member to exo groups flow including adding new users with auto retry --- .../CIPPCore/Public/Add-CIPPGroupMember.ps1 | 70 +++++++++++++------ Modules/CIPPCore/Public/New-CIPPUserTask.ps1 | 32 ++++++++- 2 files changed, 77 insertions(+), 25 deletions(-) diff --git a/Modules/CIPPCore/Public/Add-CIPPGroupMember.ps1 b/Modules/CIPPCore/Public/Add-CIPPGroupMember.ps1 index 3a5ea194fe18..547634031dfc 100644 --- a/Modules/CIPPCore/Public/Add-CIPPGroupMember.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPGroupMember.ps1 @@ -45,37 +45,54 @@ function Add-CIPPGroupMember { } $Users = New-GraphBulkRequest -Requests @($Requests) -tenantid $TenantFilter - if ($GroupType -eq 'Distribution list' -or $GroupType -eq 'Mail-Enabled Security') { + $SuccessfulUsers = [System.Collections.Generic.List[string]]::new() + $FailedUsers = [System.Collections.Generic.List[string]]::new() + + # Accept both human-readable labels (from Invoke-EditGroup / older callers) and + # camelCase calculatedGroupType values (from the user template / add-edit-user form) + $ExoGroupTypes = @('Distribution list', 'Distribution List', 'Mail-Enabled Security', 'distributionList', 'security') + + if ($GroupType -in $ExoGroupTypes) { $ExoBulkRequests = [System.Collections.Generic.List[object]]::new() - $ExoLogs = [System.Collections.Generic.List[object]]::new() + $GuidToUpn = @{} foreach ($User in $Users) { - $Params = @{ Identity = $GroupId; Member = $User.body.userPrincipalName; BypassSecurityGroupManagerCheck = $true } + $UserUpn = $User.body.userPrincipalName + if (-not $UserUpn) { continue } + $OpGuid = [guid]::NewGuid().ToString() + $GuidToUpn[$OpGuid] = $UserUpn + $Params = @{ Identity = $GroupId; Member = $UserUpn; BypassSecurityGroupManagerCheck = $true } $ExoBulkRequests.Add(@{ - CmdletInput = @{ + OperationGuid = $OpGuid + CmdletInput = @{ CmdletName = 'Add-DistributionGroupMember' Parameters = $Params } }) - $ExoLogs.Add(@{ - message = "Added member $($User.body.userPrincipalName) to $($GroupId) group" - target = $User.body.userPrincipalName - }) } if ($ExoBulkRequests.Count -gt 0) { - $RawExoRequest = New-ExoBulkRequest -tenantid $TenantFilter -cmdletArray @($ExoBulkRequests) - $LastError = $RawExoRequest | Select-Object -Last 1 + $RawExoRequest = @(New-ExoBulkRequest -tenantid $TenantFilter -cmdletArray @($ExoBulkRequests)) - foreach ($ExoError in $LastError.error) { - Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $ExoError -Sev 'Error' - throw $ExoError + # Index responses by OperationGuid so each user is correlated by position, not by error.target + $ResponseByGuid = @{} + foreach ($Response in $RawExoRequest) { + if ($Response.OperationGuid) { + $ResponseByGuid[$Response.OperationGuid] = $Response + } } - foreach ($ExoLog in $ExoLogs) { - $ExoError = $LastError | Where-Object { $ExoLog.target -in $_.target -and $_.error } - if (!$LastError -or ($LastError.error -and $LastError.target -notcontains $ExoLog.target)) { - Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $ExoLog.message -Sev 'Info' + foreach ($OpGuid in $GuidToUpn.Keys) { + $UserUpn = $GuidToUpn[$OpGuid] + $Response = $ResponseByGuid[$OpGuid] + + if ($Response -and $Response.error) { + $ErrorText = if ($Response.error -is [string]) { $Response.error } else { ($Response.error | Out-String).Trim() } + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to add member $($UserUpn) to $($GroupId): $ErrorText" -Sev 'Error' + $FailedUsers.Add($UserUpn) + } else { + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Added member $($UserUpn) to $($GroupId) group" -Sev 'Info' + $SuccessfulUsers.Add($UserUpn) } } } @@ -91,19 +108,26 @@ function Add-CIPPGroupMember { } } $AddResults = New-GraphBulkRequest -tenantid $TenantFilter -Requests @($AddRequests) - $SuccessfulUsers = [system.collections.generic.list[string]]::new() foreach ($Result in $AddResults) { + $UserPrincipalName = $Users | Where-Object { $_.body.id -eq $Result.id } | Select-Object -ExpandProperty body | Select-Object -ExpandProperty userPrincipalName if ($Result.status -lt 200 -or $Result.status -gt 299) { - $FailedUsername = $Users | Where-Object { $_.body.id -eq $Result.id } | Select-Object -ExpandProperty body | Select-Object -ExpandProperty userPrincipalName - Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to add member $($FailedUsername): $($Result.body.error.message)" -Sev 'Error' + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to add member $($UserPrincipalName): $($Result.body.error.message)" -Sev 'Error' + $FailedUsers.Add($UserPrincipalName) } else { - $UserPrincipalName = $Users | Where-Object { $_.body.id -eq $Result.id } | Select-Object -ExpandProperty body | Select-Object -ExpandProperty userPrincipalName $SuccessfulUsers.Add($UserPrincipalName) } } } - $UserList = ($SuccessfulUsers -join ', ') - $Results = "Successfully added user $UserList to $($GroupId)." + + if ($SuccessfulUsers.Count -eq 0 -and $FailedUsers.Count -gt 0) { + $Results = "Failed to add user $($FailedUsers -join ', ') to $($GroupId)." + throw $Results + } + + $Results = "Successfully added user $($SuccessfulUsers -join ', ') to $($GroupId)." + if ($FailedUsers.Count -gt 0) { + $Results = "$Results Failed to add: $($FailedUsers -join ', ')." + } Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Results -Sev 'Info' return $Results } catch { diff --git a/Modules/CIPPCore/Public/New-CIPPUserTask.ps1 b/Modules/CIPPCore/Public/New-CIPPUserTask.ps1 index 7fdfab465719..a026e122bb63 100644 --- a/Modules/CIPPCore/Public/New-CIPPUserTask.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPUserTask.ps1 @@ -70,12 +70,40 @@ function New-CIPPUserTask { # Add to groups if ($UserObj.AddToGroups) { + $ExoGroupTypes = @('Distribution list', 'Distribution List', 'Mail-Enabled Security', 'distributionList', 'security') $UserObj.AddToGroups | ForEach-Object { + $Group = $_ + $GroupType = $Group.addedFields.groupType try { - $AddMemberResult = Add-CIPPGroupMember -Headers $Headers -GroupType $_.addedFields.groupType -GroupId $_.value -Member @($CreationResults.Username) -TenantFilter $UserObj.tenantFilter + $AddMemberResult = Add-CIPPGroupMember -Headers $Headers -GroupType $GroupType -GroupId $Group.value -Member @($CreationResults.Username) -TenantFilter $UserObj.tenantFilter $Results.Add($AddMemberResult) } catch { - $Results.Add("Failed to add to group $($_.label): $_") + # EXO group adds frequently fail right after user creation due to Exchange directory replication lag. + # Schedule a delayed retry so the user lands in the group automatically once EXO sees the recipient. + if ($GroupType -in $ExoGroupTypes) { + try { + $TaskBody = [PSCustomObject]@{ + TenantFilter = $UserObj.tenantFilter + Name = "Retry Add Group Member: $($CreationResults.Username) -> $($Group.label)" + Command = @{ value = 'Add-CIPPGroupMember' } + Parameters = [PSCustomObject]@{ + GroupType = $GroupType + GroupId = $Group.value + Member = @($CreationResults.Username) + TenantFilter = $UserObj.tenantFilter + APIName = 'Add Group Member (Retry)' + } + ScheduledTime = [int64](([datetime]::UtcNow).AddMinutes(15) - (Get-Date '1/1/1970')).TotalSeconds + PostExecution = @{ Webhook = $false; Email = $false; PSA = $false } + } + $null = Add-CIPPScheduledTask -Task $TaskBody -hidden $false -Headers $Headers -DisallowDuplicateName $true + $Results.Add("Could not add $($CreationResults.Username) to $($Group.label) yet (Exchange replication delay). A retry has been scheduled in 15 minutes.") + } catch { + $Results.Add("Failed to add to group $($Group.label): $_") + } + } else { + $Results.Add("Failed to add to group $($Group.label): $_") + } } } } From a9be2729d883ffeceb8c6eaf06998fc4e6175c40 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 3 Jun 2026 14:39:44 +0800 Subject: [PATCH 114/202] Update Invoke-CIPPStandardEnableExchangeCloudManagement.ps1 --- .../Invoke-CIPPStandardEnableExchangeCloudManagement.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEnableExchangeCloudManagement.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEnableExchangeCloudManagement.ps1 index da1af84b41ac..140b4b6693e0 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEnableExchangeCloudManagement.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEnableExchangeCloudManagement.ps1 @@ -46,7 +46,8 @@ function Invoke-CIPPStandardEnableExchangeCloudManagement { return $true } - $DesiredState = [System.Convert]::ToBoolean($Settings.state) + $StateValue = $Settings.state.value ?? $Settings.state + $DesiredState = [System.Convert]::ToBoolean($StateValue) $StateText = if ($DesiredState) { 'Cloud' } else { 'On-Premises' } try { From 3e99e667d8fe5e6326cc465fa45264df817e5938 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 3 Jun 2026 13:37:04 +0200 Subject: [PATCH 115/202] add mcp allowed --- .../Public/Authentication/Get-CippApiClient.ps1 | 2 +- .../HTTP Functions/CIPP/MCP/Invoke-ExecMcp.ps1 | 14 ++++++++++++++ .../CIPP/Settings/Invoke-ExecApiClient.ps1 | 2 ++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Authentication/Get-CippApiClient.ps1 b/Modules/CIPPCore/Public/Authentication/Get-CippApiClient.ps1 index d316537c4a01..0bdd119a8575 100644 --- a/Modules/CIPPCore/Public/Authentication/Get-CippApiClient.ps1 +++ b/Modules/CIPPCore/Public/Authentication/Get-CippApiClient.ps1 @@ -20,7 +20,7 @@ function Get-CippApiClient { } $Apps = Get-CIPPAzDataTableEntity @Table | Where-Object { ![string]::IsNullOrEmpty($_.RowKey) } $Apps = foreach ($Client in $Apps) { - $Client = $Client | Select-Object -Property @{Name = 'ClientId'; Expression = { $_.RowKey } }, AppName, Role, IPRange, Enabled + $Client = $Client | Select-Object -Property @{Name = 'ClientId'; Expression = { $_.RowKey } }, AppName, Role, IPRange, Enabled, @{Name = 'MCPAllowed'; Expression = { [bool]$_.MCPAllowed } } if (!$Client.Role) { $Client.Role = $null diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/MCP/Invoke-ExecMcp.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/MCP/Invoke-ExecMcp.ps1 index 19514f9db6fa..ddb3a8e8dd27 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/MCP/Invoke-ExecMcp.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/MCP/Invoke-ExecMcp.ps1 @@ -27,6 +27,20 @@ function Invoke-ExecMcp { }) } + # Per-client gate: the global 'MCPServer' feature flag lets this endpoint run at all (enforced + # upstream in New-CippCoreRequest); this narrows access to API clients explicitly flagged + # 'MCP Access Allowed'. A non-API-client caller, an unknown client, or one without the flag is denied. + $CallerAppId = $Request.Headers.'x-ms-client-principal-name' + $IsApiClient = $Request.Headers.'x-ms-client-principal-idp' -eq 'aad' -and $CallerAppId -match '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' + $McpAllowed = if ($IsApiClient) { [bool](Get-CippApiClient -AppId $CallerAppId).MCPAllowed } else { $false } + if (-not $McpAllowed) { + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::Forbidden + Headers = @{ 'Content-Type' = 'application/json' } + Body = (@{ jsonrpc = '2.0'; id = $null; error = @{ code = -32001; message = 'This API client is not permitted to use the MCP server. Enable "MCP Access Allowed" on the API client in CIPP.' } } | ConvertTo-Json -Compress) + }) + } + $Rpc = $Request.Body $RpcId = $Rpc.id diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecApiClient.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecApiClient.ps1 index 24eeb46c9b27..f0759904517c 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecApiClient.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecApiClient.ps1 @@ -103,6 +103,7 @@ function Invoke-ExecApiClient { $Client.Role = [string]$Request.Body.Role.value $Client.IPRange = "$(@($IpRange) | ConvertTo-Json -Compress)" $Client.Enabled = $Request.Body.Enabled ?? $false + $Client | Add-Member -NotePropertyName 'MCPAllowed' -NotePropertyValue ([bool]($Request.Body.MCPAllowed ?? $false)) -Force Write-LogMessage -headers $Request.Headers -API 'ExecApiClient' -message "Updated API client $($Request.Body.ClientId)" -Sev 'Info' if ($APIConfig.ApplicationSecret) { $Results.Add(@{ @@ -121,6 +122,7 @@ function Invoke-ExecApiClient { 'Role' = [string]$Request.Body.Role.value 'IPRange' = "$(@($IpRange) | ConvertTo-Json -Compress)" 'Enabled' = $Request.Body.Enabled ?? $false + 'MCPAllowed' = [bool]($Request.Body.MCPAllowed ?? $false) } $Results.Add(@{ resultText = "API Client created with the name '$($Client.AppName)'. Use the Copy to Clipboard button to retrieve the secret." From a2c2524c02ad0a6e66440beef1091f7a05714808 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 3 Jun 2026 16:16:02 +0200 Subject: [PATCH 116/202] oauth prm --- .../CIPP/Settings/Invoke-ExecApiClient.ps1 | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecApiClient.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecApiClient.ps1 index f0759904517c..37b9186c5e0b 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecApiClient.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecApiClient.ps1 @@ -195,6 +195,19 @@ function Invoke-ExecApiClient { $ClientIds = $AllClients.RowKey try { Set-CippApiAuth -RGName $RGName -FunctionAppName $FunctionAppName -TenantId $TenantId -ClientIds $ClientIds + + # Advertise OAuth Protected Resource Metadata scopes for MCP-enabled API clients so that + # App Service's PRM endpoint (/.well-known/oauth-protected-resource) hands MCP clients + # (e.g. Claude) a token audience Entra/EasyAuth will accept. One delegated scope per + # MCP-allowed, enabled client (api:///user_impersonation); cleared when none. + $McpScopes = @($AllClients | Where-Object { $_.MCPAllowed -eq $true } | ForEach-Object { "api://$($_.RowKey)/user_impersonation" }) -join ',' + if ($McpScopes) { + $null = Update-CIPPAzFunctionAppSetting -Name $FunctionAppName -ResourceGroupName $RGName -AppSetting @{ 'WEBSITE_AUTH_PRM_DEFAULT_WITH_SCOPES' = $McpScopes } + Write-LogMessage -headers $Request.Headers -API 'ExecApiClient' -message "Set MCP PRM scopes: $McpScopes" -Sev 'Info' + } else { + $null = Update-CIPPAzFunctionAppSetting -Name $FunctionAppName -ResourceGroupName $RGName -AppSetting @{} -RemoveKeys @('WEBSITE_AUTH_PRM_DEFAULT_WITH_SCOPES') + } + $Body = @{ Results = 'API clients saved to Azure' } Write-LogMessage -headers $Request.Headers -API 'ExecApiClient' -message 'Saved API clients to Azure' -Sev 'Info' } catch { From 1fc4263123219960af1ec87558f8cd73b7a62a7f Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Wed, 3 Jun 2026 16:54:13 +0200 Subject: [PATCH 117/202] feat: add Email as alternate login ID standard --- ...ke-CIPPStandardEmailAsAlternateLoginId.ps1 | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEmailAsAlternateLoginId.ps1 diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEmailAsAlternateLoginId.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEmailAsAlternateLoginId.ps1 new file mode 100644 index 000000000000..23029c3df6eb --- /dev/null +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEmailAsAlternateLoginId.ps1 @@ -0,0 +1,126 @@ +function Invoke-CIPPStandardEmailAsAlternateLoginId { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) EmailAsAlternateLoginId + .SYNOPSIS + (Label) Configure Email as alternate login ID + .DESCRIPTION + (Helptext) Configures the tenant-wide Email as alternate login ID setting in Home Realm Discovery policy. + (DocsDescription) Sets the Home Realm Discovery policy AlternateIdLogin setting to enable or disable using email as an alternate sign-in ID. + .NOTES + CAT + Entra (AAD) Standards + TAG + EXECUTIVETEXT + Controls whether users can sign in with email as an alternate identifier, allowing organizations to align sign-in behavior with their identity strategy and reduce authentication ambiguity. + ADDEDCOMPONENT + {"type":"switch","name":"standards.EmailAsAlternateLoginId.Enabled","label":"Enable Email as Alternate Login ID","defaultValue":false} + IMPACT + Medium Impact + ADDEDDATE + 2026-06-03 + POWERSHELLEQUIVALENT + Invoke-MgGraphRequest https://graph.microsoft.com/v1.0/policies/homeRealmDiscoveryPolicies/ + RECOMMENDEDBY + "CIPP" + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + .LINK + https://docs.cipp.app/user-documentation/tenant/standards/alignment/templates/available-standards + #> + + param($Tenant, $Settings) + + $DesiredEnabledValue = $Settings.Enabled.value ?? $Settings.Enabled ?? $false + $DesiredEnabled = if ($DesiredEnabledValue -is [bool]) { + $DesiredEnabledValue + } elseif ($DesiredEnabledValue -is [string]) { + $DesiredEnabledValue -eq 'true' + } else { + [bool]$DesiredEnabledValue + } + $DesiredStatus = if ($DesiredEnabled) { 'enabled' } else { 'disabled' } + + try { + $Policies = @(New-GraphGetRequest -Uri 'https://graph.microsoft.com/v1.0/policies/homeRealmDiscoveryPolicies' -tenantid $Tenant) + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the EmailAsAlternateLoginId state for $Tenant. Error: $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage + return + } + + $CurrentPolicy = @($Policies | Where-Object { $_.isOrganizationDefault -eq $true }) | Select-Object -First 1 + $CurrentDefinition = if ($CurrentPolicy.definition) { + ($CurrentPolicy.definition | Select-Object -First 1) | ConvertFrom-Json -ErrorAction SilentlyContinue + } else { + $null + } + $CurrentEnabledRaw = $CurrentDefinition.HomeRealmDiscoveryPolicy.AlternateIdLogin.Enabled + $PolicyExists = $null -ne $CurrentPolicy + $HasExplicitSetting = $null -ne $CurrentEnabledRaw + $CurrentEnabled = if ($null -eq $CurrentEnabledRaw) { $false } else { [bool]$CurrentEnabledRaw } + $StateIsCorrect = $PolicyExists -and $HasExplicitSetting -and ($CurrentEnabled -eq $DesiredEnabled) + + $CurrentValue = [PSCustomObject]@{ + AlternateIdLoginEnabled = $CurrentEnabled + PolicyExists = $PolicyExists + HasExplicitSetting = $HasExplicitSetting + } + $ExpectedValue = [PSCustomObject]@{ + AlternateIdLoginEnabled = $DesiredEnabled + PolicyExists = $true + HasExplicitSetting = $true + } + + if ($Settings.remediate -eq $true) { + if ($StateIsCorrect) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Email as alternate login ID is already $DesiredStatus." -sev Info + } else { + try { + $PolicyDefinition = @{ + HomeRealmDiscoveryPolicy = @{ + AlternateIdLogin = @{ + Enabled = $DesiredEnabled + } + } + } | ConvertTo-Json -Depth 10 -Compress + + $Body = @{ + definition = @($PolicyDefinition) + isOrganizationDefault = $true + displayName = 'HomeRealmDiscoveryPolicy' + } | ConvertTo-Json -Depth 10 -Compress + + if ($PolicyExists) { + $RequestUri = "https://graph.microsoft.com/v1.0/policies/homeRealmDiscoveryPolicies/$($CurrentPolicy.id)" + $RequestType = 'PATCH' + } else { + $RequestUri = 'https://graph.microsoft.com/v1.0/policies/homeRealmDiscoveryPolicies/' + $RequestType = 'POST' + } + + New-GraphPostRequest -tenantid $Tenant -Uri $RequestUri -Type $RequestType -Body $Body | Out-Null + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Set Email as alternate login ID to $DesiredStatus." -sev Info + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to set Email as alternate login ID to $DesiredStatus. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + } + } + } + + if ($Settings.alert -eq $true) { + if ($StateIsCorrect) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Email as alternate login ID is $DesiredStatus." -sev Info + } else { + Write-StandardsAlert -message "Email as alternate login ID is not $DesiredStatus." -object $CurrentValue -tenant $Tenant -standardName 'EmailAsAlternateLoginId' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Email as alternate login ID is not $DesiredStatus." -sev Info + } + } + + if ($Settings.report -eq $true) { + Set-CIPPStandardsCompareField -FieldName 'standards.EmailAsAlternateLoginId' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant + Add-CIPPBPAField -FieldName 'EmailAsAlternateLoginId' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant + } +} From 5c81043c08757d3b340414b9fe2d865c1a1ca612 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 3 Jun 2026 17:41:01 +0200 Subject: [PATCH 118/202] change resource usage to craft well known --- CIPPHttpTrigger/function.json | 2 +- CIPPWellKnown/function.json | 21 ++++++ .../Authentication/Set-CIPPSSOEasyAuth.ps1 | 12 ++++ .../Public/Authentication/Set-CippApiAuth.ps1 | 11 ++++ .../CIPP/Settings/Invoke-ExecApiClient.ps1 | 15 ++--- Modules/CippEntrypoints/CippEntrypoints.psm1 | 66 ++++++++++++++++++- host.json | 3 + 7 files changed, 117 insertions(+), 13 deletions(-) create mode 100644 CIPPWellKnown/function.json diff --git a/CIPPHttpTrigger/function.json b/CIPPHttpTrigger/function.json index 643fa9b6a3ad..d62caa1abbcc 100644 --- a/CIPPHttpTrigger/function.json +++ b/CIPPHttpTrigger/function.json @@ -15,7 +15,7 @@ "delete", "options" ], - "route": "{*CIPPEndpoint}" + "route": "api/{*CIPPEndpoint}" }, { "type": "http", diff --git a/CIPPWellKnown/function.json b/CIPPWellKnown/function.json new file mode 100644 index 000000000000..72706d947ac1 --- /dev/null +++ b/CIPPWellKnown/function.json @@ -0,0 +1,21 @@ +{ + "scriptFile": "../Modules/CippEntrypoints/CippEntrypoints.psm1", + "entryPoint": "Receive-CippWellKnownTrigger", + "bindings": [ + { + "authLevel": "anonymous", + "type": "httpTrigger", + "direction": "in", + "name": "Request", + "methods": [ + "get" + ], + "route": ".well-known/{*rest}" + }, + { + "type": "http", + "direction": "out", + "name": "Response" + } + ] +} diff --git a/Modules/CIPPCore/Public/Authentication/Set-CIPPSSOEasyAuth.ps1 b/Modules/CIPPCore/Public/Authentication/Set-CIPPSSOEasyAuth.ps1 index 8ced685da842..58dc7ec102c0 100644 --- a/Modules/CIPPCore/Public/Authentication/Set-CIPPSSOEasyAuth.ps1 +++ b/Modules/CIPPCore/Public/Authentication/Set-CIPPSSOEasyAuth.ps1 @@ -125,6 +125,17 @@ function Set-CIPPSSOEasyAuth { $AAD.validation.defaultAuthorizationPolicy.allowedPrincipals = @{} } + # Ensure the MCP protected-resource metadata path stays anonymous so CIPP can serve it + if (-not $Current.ContainsKey('globalValidation') -or $null -eq $Current.globalValidation) { $Current.globalValidation = @{} } + $ExcludedPaths = [System.Collections.Generic.List[string]]::new() + if ($Current.globalValidation.excludedPaths) { + foreach ($ExPath in $Current.globalValidation.excludedPaths) { $ExcludedPaths.Add($ExPath) } + } + if ($ExcludedPaths -notcontains '/.well-known/oauth-protected-resource*') { + $ExcludedPaths.Add('/.well-known/oauth-protected-resource*') + } + $Current.globalValidation.excludedPaths = @($ExcludedPaths) + $AuthConfig = $ArmPayload | ConvertTo-Json -Depth 20 Write-Information "[SSO-EasyAuth] Read-modify-write: patching issuer to $IssuerUrl (preserving $(($ExistingAudiences).Count) audiences, $(($ExistingApps).Count) allowed apps)" } else { @@ -138,6 +149,7 @@ function Set-CIPPSSOEasyAuth { excludedPaths = @( '/api/Public*' '/api/setup/health' + '/.well-known/oauth-protected-resource*' ) } identityProviders = @{ diff --git a/Modules/CIPPCore/Public/Authentication/Set-CippApiAuth.ps1 b/Modules/CIPPCore/Public/Authentication/Set-CippApiAuth.ps1 index f9e421b24a31..fddf4f8c52a5 100644 --- a/Modules/CIPPCore/Public/Authentication/Set-CippApiAuth.ps1 +++ b/Modules/CIPPCore/Public/Authentication/Set-CippApiAuth.ps1 @@ -82,6 +82,17 @@ function Set-CippApiAuth { $AAD.validation.defaultAuthorizationPolicy.allowedPrincipals = @{} } + # Keep the MCP protected-resource metadata path anonymous so CIPP can serve it at the root + if (-not $Current.ContainsKey('globalValidation') -or $null -eq $Current.globalValidation) { $Current.globalValidation = @{} } + $ExcludedPaths = [System.Collections.Generic.List[string]]::new() + if ($Current.globalValidation.excludedPaths) { + foreach ($ExPath in $Current.globalValidation.excludedPaths) { $ExcludedPaths.Add($ExPath) } + } + if ($ExcludedPaths -notcontains '/.well-known/oauth-protected-resource*') { + $ExcludedPaths.Add('/.well-known/oauth-protected-resource*') + } + $Current.globalValidation.excludedPaths = @($ExcludedPaths) + $PutBody = $ArmPayload | ConvertTo-Json -Depth 20 Write-Information "[ApiAuth] PUT body: $PutBody" diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecApiClient.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecApiClient.ps1 index 37b9186c5e0b..c46933eb0484 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecApiClient.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecApiClient.ps1 @@ -196,17 +196,10 @@ function Invoke-ExecApiClient { try { Set-CippApiAuth -RGName $RGName -FunctionAppName $FunctionAppName -TenantId $TenantId -ClientIds $ClientIds - # Advertise OAuth Protected Resource Metadata scopes for MCP-enabled API clients so that - # App Service's PRM endpoint (/.well-known/oauth-protected-resource) hands MCP clients - # (e.g. Claude) a token audience Entra/EasyAuth will accept. One delegated scope per - # MCP-allowed, enabled client (api:///user_impersonation); cleared when none. - $McpScopes = @($AllClients | Where-Object { $_.MCPAllowed -eq $true } | ForEach-Object { "api://$($_.RowKey)/user_impersonation" }) -join ',' - if ($McpScopes) { - $null = Update-CIPPAzFunctionAppSetting -Name $FunctionAppName -ResourceGroupName $RGName -AppSetting @{ 'WEBSITE_AUTH_PRM_DEFAULT_WITH_SCOPES' = $McpScopes } - Write-LogMessage -headers $Request.Headers -API 'ExecApiClient' -message "Set MCP PRM scopes: $McpScopes" -Sev 'Info' - } else { - $null = Update-CIPPAzFunctionAppSetting -Name $FunctionAppName -ResourceGroupName $RGName -AppSetting @{} -RemoveKeys @('WEBSITE_AUTH_PRM_DEFAULT_WITH_SCOPES') - } + # MCP OAuth Protected Resource Metadata is served by CIPP itself (Receive-CippWellKnownTrigger), + # so the App Service platform PRM must stay off to avoid serving a competing document at + # /.well-known/oauth-protected-resource. Clear the setting if a previous build enabled it. + $null = Update-CIPPAzFunctionAppSetting -Name $FunctionAppName -ResourceGroupName $RGName -AppSetting @{} -RemoveKeys @('WEBSITE_AUTH_PRM_DEFAULT_WITH_SCOPES') $Body = @{ Results = 'API clients saved to Azure' } Write-LogMessage -headers $Request.Headers -API 'ExecApiClient' -message 'Saved API clients to Azure' -Sev 'Info' diff --git a/Modules/CippEntrypoints/CippEntrypoints.psm1 b/Modules/CippEntrypoints/CippEntrypoints.psm1 index 1f953f3055a3..e1c1679768b6 100644 --- a/Modules/CippEntrypoints/CippEntrypoints.psm1 +++ b/Modules/CippEntrypoints/CippEntrypoints.psm1 @@ -575,5 +575,69 @@ function Receive-CIPPTimerTrigger { return $true } -Export-ModuleMember -Function @('Receive-CippHttpTrigger', 'Receive-CippQueueTrigger', 'Receive-CippOrchestrationTrigger', 'Receive-CippActivityTrigger', 'Receive-CIPPTimerTrigger') +function Receive-CippWellKnownTrigger { + <# + .SYNOPSIS + Serves OAuth 2.0 Protected Resource Metadata (RFC 9728) for the MCP server. + .DESCRIPTION + Responds at the root path /.well-known/oauth-protected-resource (and the path-aware + variant) so MCP clients (e.g. Claude) discover the correct token audience. The advertised + 'resource' is the EasyAuth app's Application ID URI (api://) rather than the raw + server URL, so Entra issues a token whose audience EasyAuth accepts (clears AADSTS9010010). + Values are derived per-instance from the EasyAuth configuration; nothing is hardcoded. + .FUNCTIONALITY + Entrypoint + #> + param($Request, $TriggerMetadata) + + # httpContext route params are case-sensitive since PS 7.3; normalize like the main trigger. + $Request = $Request | ConvertTo-Json -Depth 10 | ConvertFrom-Json + $Rest = $Request.Params.rest + + if ($Rest -ne 'oauth-protected-resource' -and $Rest -notlike 'oauth-protected-resource/*') { + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::NotFound + }) + return + } + + $SSOClientId = $null + $Issuer = $null + try { + if ($env:WEBSITE_AUTH_V2_CONFIG_JSON) { + $AuthConfig = $env:WEBSITE_AUTH_V2_CONFIG_JSON | ConvertFrom-Json + $AAD = $AuthConfig.identityProviders.azureActiveDirectory + $SSOClientId = $AAD.registration.clientId + $Issuer = $AAD.registration.openIdIssuer + } + } catch { + Write-Information "WellKnown: failed to parse EasyAuth config: $($_.Exception.Message)" + } + + if (-not $Issuer) { $Issuer = "https://login.microsoftonline.com/$($env:TenantID)/v2.0" } + + if (-not $SSOClientId) { + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::ServiceUnavailable + Headers = @{ 'Content-Type' = 'application/json' } + Body = (@{ error = 'EasyAuth (SSO) is not configured; cannot publish protected resource metadata.' } | ConvertTo-Json -Compress) + }) + return + } + + $Metadata = [ordered]@{ + resource = "api://$SSOClientId" + authorization_servers = @($Issuer) + scopes_supported = @("api://$SSOClientId/user_impersonation") + bearer_methods_supported = @('header') + } + + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Headers = @{ 'Content-Type' = 'application/json'; 'Access-Control-Allow-Origin' = '*'; 'Cache-Control' = 'no-store' } + Body = ($Metadata | ConvertTo-Json -Depth 5 -Compress) + }) +} + +Export-ModuleMember -Function @('Receive-CippHttpTrigger', 'Receive-CippQueueTrigger', 'Receive-CippOrchestrationTrigger', 'Receive-CippActivityTrigger', 'Receive-CIPPTimerTrigger', 'Receive-CippWellKnownTrigger') diff --git a/host.json b/host.json index 5635adf20ae2..5e35b7d41570 100644 --- a/host.json +++ b/host.json @@ -19,6 +19,9 @@ "defaultVersion": "10.4.5", "versionMatchStrategy": "Strict", "versionFailureStrategy": "Fail" + }, + "http": { + "routePrefix": "" } } } From 6fc7d357ee34ea694b96028815d2f9eacfc715e5 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 3 Jun 2026 18:18:49 +0200 Subject: [PATCH 119/202] Revert custom well known --- CIPPHttpTrigger/function.json | 2 +- CIPPWellKnown/function.json | 21 ------ .../Authentication/Set-CIPPSSOEasyAuth.ps1 | 12 ---- .../Public/Authentication/Set-CippApiAuth.ps1 | 11 ---- .../CIPP/Settings/Invoke-ExecApiClient.ps1 | 6 -- Modules/CippEntrypoints/CippEntrypoints.psm1 | 66 +------------------ host.json | 3 - 7 files changed, 2 insertions(+), 119 deletions(-) delete mode 100644 CIPPWellKnown/function.json diff --git a/CIPPHttpTrigger/function.json b/CIPPHttpTrigger/function.json index d62caa1abbcc..643fa9b6a3ad 100644 --- a/CIPPHttpTrigger/function.json +++ b/CIPPHttpTrigger/function.json @@ -15,7 +15,7 @@ "delete", "options" ], - "route": "api/{*CIPPEndpoint}" + "route": "{*CIPPEndpoint}" }, { "type": "http", diff --git a/CIPPWellKnown/function.json b/CIPPWellKnown/function.json deleted file mode 100644 index 72706d947ac1..000000000000 --- a/CIPPWellKnown/function.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "scriptFile": "../Modules/CippEntrypoints/CippEntrypoints.psm1", - "entryPoint": "Receive-CippWellKnownTrigger", - "bindings": [ - { - "authLevel": "anonymous", - "type": "httpTrigger", - "direction": "in", - "name": "Request", - "methods": [ - "get" - ], - "route": ".well-known/{*rest}" - }, - { - "type": "http", - "direction": "out", - "name": "Response" - } - ] -} diff --git a/Modules/CIPPCore/Public/Authentication/Set-CIPPSSOEasyAuth.ps1 b/Modules/CIPPCore/Public/Authentication/Set-CIPPSSOEasyAuth.ps1 index 58dc7ec102c0..8ced685da842 100644 --- a/Modules/CIPPCore/Public/Authentication/Set-CIPPSSOEasyAuth.ps1 +++ b/Modules/CIPPCore/Public/Authentication/Set-CIPPSSOEasyAuth.ps1 @@ -125,17 +125,6 @@ function Set-CIPPSSOEasyAuth { $AAD.validation.defaultAuthorizationPolicy.allowedPrincipals = @{} } - # Ensure the MCP protected-resource metadata path stays anonymous so CIPP can serve it - if (-not $Current.ContainsKey('globalValidation') -or $null -eq $Current.globalValidation) { $Current.globalValidation = @{} } - $ExcludedPaths = [System.Collections.Generic.List[string]]::new() - if ($Current.globalValidation.excludedPaths) { - foreach ($ExPath in $Current.globalValidation.excludedPaths) { $ExcludedPaths.Add($ExPath) } - } - if ($ExcludedPaths -notcontains '/.well-known/oauth-protected-resource*') { - $ExcludedPaths.Add('/.well-known/oauth-protected-resource*') - } - $Current.globalValidation.excludedPaths = @($ExcludedPaths) - $AuthConfig = $ArmPayload | ConvertTo-Json -Depth 20 Write-Information "[SSO-EasyAuth] Read-modify-write: patching issuer to $IssuerUrl (preserving $(($ExistingAudiences).Count) audiences, $(($ExistingApps).Count) allowed apps)" } else { @@ -149,7 +138,6 @@ function Set-CIPPSSOEasyAuth { excludedPaths = @( '/api/Public*' '/api/setup/health' - '/.well-known/oauth-protected-resource*' ) } identityProviders = @{ diff --git a/Modules/CIPPCore/Public/Authentication/Set-CippApiAuth.ps1 b/Modules/CIPPCore/Public/Authentication/Set-CippApiAuth.ps1 index fddf4f8c52a5..f9e421b24a31 100644 --- a/Modules/CIPPCore/Public/Authentication/Set-CippApiAuth.ps1 +++ b/Modules/CIPPCore/Public/Authentication/Set-CippApiAuth.ps1 @@ -82,17 +82,6 @@ function Set-CippApiAuth { $AAD.validation.defaultAuthorizationPolicy.allowedPrincipals = @{} } - # Keep the MCP protected-resource metadata path anonymous so CIPP can serve it at the root - if (-not $Current.ContainsKey('globalValidation') -or $null -eq $Current.globalValidation) { $Current.globalValidation = @{} } - $ExcludedPaths = [System.Collections.Generic.List[string]]::new() - if ($Current.globalValidation.excludedPaths) { - foreach ($ExPath in $Current.globalValidation.excludedPaths) { $ExcludedPaths.Add($ExPath) } - } - if ($ExcludedPaths -notcontains '/.well-known/oauth-protected-resource*') { - $ExcludedPaths.Add('/.well-known/oauth-protected-resource*') - } - $Current.globalValidation.excludedPaths = @($ExcludedPaths) - $PutBody = $ArmPayload | ConvertTo-Json -Depth 20 Write-Information "[ApiAuth] PUT body: $PutBody" diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecApiClient.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecApiClient.ps1 index c46933eb0484..f0759904517c 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecApiClient.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecApiClient.ps1 @@ -195,12 +195,6 @@ function Invoke-ExecApiClient { $ClientIds = $AllClients.RowKey try { Set-CippApiAuth -RGName $RGName -FunctionAppName $FunctionAppName -TenantId $TenantId -ClientIds $ClientIds - - # MCP OAuth Protected Resource Metadata is served by CIPP itself (Receive-CippWellKnownTrigger), - # so the App Service platform PRM must stay off to avoid serving a competing document at - # /.well-known/oauth-protected-resource. Clear the setting if a previous build enabled it. - $null = Update-CIPPAzFunctionAppSetting -Name $FunctionAppName -ResourceGroupName $RGName -AppSetting @{} -RemoveKeys @('WEBSITE_AUTH_PRM_DEFAULT_WITH_SCOPES') - $Body = @{ Results = 'API clients saved to Azure' } Write-LogMessage -headers $Request.Headers -API 'ExecApiClient' -message 'Saved API clients to Azure' -Sev 'Info' } catch { diff --git a/Modules/CippEntrypoints/CippEntrypoints.psm1 b/Modules/CippEntrypoints/CippEntrypoints.psm1 index e1c1679768b6..1f953f3055a3 100644 --- a/Modules/CippEntrypoints/CippEntrypoints.psm1 +++ b/Modules/CippEntrypoints/CippEntrypoints.psm1 @@ -575,69 +575,5 @@ function Receive-CIPPTimerTrigger { return $true } -function Receive-CippWellKnownTrigger { - <# - .SYNOPSIS - Serves OAuth 2.0 Protected Resource Metadata (RFC 9728) for the MCP server. - .DESCRIPTION - Responds at the root path /.well-known/oauth-protected-resource (and the path-aware - variant) so MCP clients (e.g. Claude) discover the correct token audience. The advertised - 'resource' is the EasyAuth app's Application ID URI (api://) rather than the raw - server URL, so Entra issues a token whose audience EasyAuth accepts (clears AADSTS9010010). - Values are derived per-instance from the EasyAuth configuration; nothing is hardcoded. - .FUNCTIONALITY - Entrypoint - #> - param($Request, $TriggerMetadata) - - # httpContext route params are case-sensitive since PS 7.3; normalize like the main trigger. - $Request = $Request | ConvertTo-Json -Depth 10 | ConvertFrom-Json - $Rest = $Request.Params.rest - - if ($Rest -ne 'oauth-protected-resource' -and $Rest -notlike 'oauth-protected-resource/*') { - Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ - StatusCode = [HttpStatusCode]::NotFound - }) - return - } - - $SSOClientId = $null - $Issuer = $null - try { - if ($env:WEBSITE_AUTH_V2_CONFIG_JSON) { - $AuthConfig = $env:WEBSITE_AUTH_V2_CONFIG_JSON | ConvertFrom-Json - $AAD = $AuthConfig.identityProviders.azureActiveDirectory - $SSOClientId = $AAD.registration.clientId - $Issuer = $AAD.registration.openIdIssuer - } - } catch { - Write-Information "WellKnown: failed to parse EasyAuth config: $($_.Exception.Message)" - } - - if (-not $Issuer) { $Issuer = "https://login.microsoftonline.com/$($env:TenantID)/v2.0" } - - if (-not $SSOClientId) { - Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ - StatusCode = [HttpStatusCode]::ServiceUnavailable - Headers = @{ 'Content-Type' = 'application/json' } - Body = (@{ error = 'EasyAuth (SSO) is not configured; cannot publish protected resource metadata.' } | ConvertTo-Json -Compress) - }) - return - } - - $Metadata = [ordered]@{ - resource = "api://$SSOClientId" - authorization_servers = @($Issuer) - scopes_supported = @("api://$SSOClientId/user_impersonation") - bearer_methods_supported = @('header') - } - - Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ - StatusCode = [HttpStatusCode]::OK - Headers = @{ 'Content-Type' = 'application/json'; 'Access-Control-Allow-Origin' = '*'; 'Cache-Control' = 'no-store' } - Body = ($Metadata | ConvertTo-Json -Depth 5 -Compress) - }) -} - -Export-ModuleMember -Function @('Receive-CippHttpTrigger', 'Receive-CippQueueTrigger', 'Receive-CippOrchestrationTrigger', 'Receive-CippActivityTrigger', 'Receive-CIPPTimerTrigger', 'Receive-CippWellKnownTrigger') +Export-ModuleMember -Function @('Receive-CippHttpTrigger', 'Receive-CippQueueTrigger', 'Receive-CippOrchestrationTrigger', 'Receive-CippActivityTrigger', 'Receive-CIPPTimerTrigger') diff --git a/host.json b/host.json index 5e35b7d41570..5635adf20ae2 100644 --- a/host.json +++ b/host.json @@ -19,9 +19,6 @@ "defaultVersion": "10.4.5", "versionMatchStrategy": "Strict", "versionFailureStrategy": "Fail" - }, - "http": { - "routePrefix": "" } } } From 0cd6f9d2d630e1c9561103fa4f3c1fefa95d33a5 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 3 Jun 2026 19:25:55 +0200 Subject: [PATCH 120/202] MCP client updates to support client auth --- .../Authentication/Set-CIPPMCPClientApp.ps1 | 79 +++++++++++++++++++ .../Public/Authentication/Set-CippApiAuth.ps1 | 13 ++- .../CIPP/Settings/Invoke-ExecApiClient.ps1 | 26 +++++- 3 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 Modules/CIPPCore/Public/Authentication/Set-CIPPMCPClientApp.ps1 diff --git a/Modules/CIPPCore/Public/Authentication/Set-CIPPMCPClientApp.ps1 b/Modules/CIPPCore/Public/Authentication/Set-CIPPMCPClientApp.ps1 new file mode 100644 index 000000000000..a5142f56be03 --- /dev/null +++ b/Modules/CIPPCore/Public/Authentication/Set-CIPPMCPClientApp.ps1 @@ -0,0 +1,79 @@ +function Set-CIPPMCPClientApp { + <# + .SYNOPSIS + Configures an API client's app registration to act as the MCP OAuth resource. + .DESCRIPTION +Sets a cipp API client. + .PARAMETER AppId + Application (client) ID of the API client to configure. + .FUNCTIONALITY + Internal + #> + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(Mandatory)] + [string]$AppId, + $Headers + ) + + $Hostname = $env:WEBSITE_HOSTNAME + if ([string]::IsNullOrWhiteSpace($Hostname)) { + throw 'WEBSITE_HOSTNAME is not set; cannot determine the MCP resource URL.' + } + + $McpUris = @("https://$Hostname", "https://$Hostname/api/ExecMcp") + + $App = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/applications(appId='$AppId')" -NoAuthCheck $true -AsApp $true + if (-not $App) { + throw "App registration with AppId '$AppId' was not found." + } + + # Merge identifier URIs, preserving existing (e.g. api://) + $IdentifierUris = [System.Collections.Generic.List[string]]::new() + foreach ($Uri in @($App.identifierUris)) { + if (-not [string]::IsNullOrWhiteSpace($Uri) -and $IdentifierUris -notcontains $Uri) { $IdentifierUris.Add($Uri) } + } + foreach ($Uri in $McpUris) { + if ($IdentifierUris -notcontains $Uri) { $IdentifierUris.Add($Uri) } + } + + # Preserve the existing api object; force v2 tokens; ensure a user_impersonation delegated scope + $Api = if ($App.api) { $App.api | ConvertTo-Json -Depth 10 | ConvertFrom-Json -AsHashtable } else { @{} } + $Api.requestedAccessTokenVersion = 2 + $Scopes = [System.Collections.Generic.List[object]]::new() + if ($Api.oauth2PermissionScopes) { + foreach ($Scope in $Api.oauth2PermissionScopes) { $Scopes.Add($Scope) } + } + if (-not ($Scopes | Where-Object { $_.value -eq 'user_impersonation' })) { + $Scopes.Add(@{ + adminConsentDescription = 'Allow the application to access CIPP-API on behalf of the signed-in user.' + adminConsentDisplayName = 'Access CIPP-API' + id = [guid]::NewGuid().ToString() + isEnabled = $true + type = 'User' + userConsentDescription = 'Allow the application to access CIPP-API on your behalf.' + userConsentDisplayName = 'Access CIPP-API' + value = 'user_impersonation' + }) + } + $Api.oauth2PermissionScopes = @($Scopes) + + $PatchBody = @{ + identifierUris = @($IdentifierUris) + api = $Api + } | ConvertTo-Json -Depth 10 -Compress + + if ($PSCmdlet.ShouldProcess($AppId, 'Configure app registration for MCP')) { + try { + $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/v1.0/applications/$($App.id)" -type PATCH -body $PatchBody -NoAuthCheck $true -asapp $true + Write-LogMessage -headers $Headers -API 'ExecApiClient' -message "Configured app registration $AppId as MCP resource (identifier URIs + v2 tokens)." -Sev 'Info' + return @{ Success = $true; IdentifierUris = @($IdentifierUris) } + } catch { + $ErrMsg = $_.Exception.Message + if ($ErrMsg -match 'identifierUri' -or $ErrMsg -match 'already exists' -or $ErrMsg -match 'in use') { + throw "The MCP resource URIs are already assigned to another application. Only one API client can be the MCP resource client. ($ErrMsg)" + } + throw + } + } +} diff --git a/Modules/CIPPCore/Public/Authentication/Set-CippApiAuth.ps1 b/Modules/CIPPCore/Public/Authentication/Set-CippApiAuth.ps1 index f9e421b24a31..884ff484bc37 100644 --- a/Modules/CIPPCore/Public/Authentication/Set-CippApiAuth.ps1 +++ b/Modules/CIPPCore/Public/Authentication/Set-CippApiAuth.ps1 @@ -4,7 +4,8 @@ function Set-CippApiAuth { [string]$RGName, [string]$FunctionAppName, [string]$TenantId, - [string[]]$ClientIds + [string[]]$ClientIds, + [string[]]$McpClientIds ) if ($env:CIPPNG) { @@ -63,6 +64,16 @@ function Set-CippApiAuth { [void]$AllAudiences.Add("api://$id") } + # MCP resource clients also accept tokens whose audience is the host-based identifier URI or + # the bare appId (v2 tokens), so the Claude connector's token validates against EasyAuth. + if ($McpClientIds -and $env:WEBSITE_HOSTNAME) { + [void]$AllAudiences.Add("https://$($env:WEBSITE_HOSTNAME)") + [void]$AllAudiences.Add("https://$($env:WEBSITE_HOSTNAME)/api/ExecMcp") + foreach ($McpId in $McpClientIds) { + if (-not [string]::IsNullOrEmpty($McpId)) { [void]$AllAudiences.Add($McpId) } + } + } + Write-Information "[ApiAuth] Merged allowedApplications: $($AllAppIds -join ', ')" Write-Information "[ApiAuth] Merged allowedAudiences: $($AllAudiences -join ', ')" diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecApiClient.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecApiClient.ps1 index f0759904517c..5175af03704b 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecApiClient.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecApiClient.ps1 @@ -132,6 +132,20 @@ function Invoke-ExecApiClient { } Add-CIPPAzDataTableEntity @Table -Entity $Client -Force | Out-Null + + # When this client is MCP-enabled, configure its app registration as the MCP OAuth + # resource (host identifier URIs + v2 tokens) so the Claude connector flow can resolve it. + if ([bool]($Request.Body.MCPAllowed ?? $false)) { + try { + $null = Set-CIPPMCPClientApp -AppId $ClientId -Headers $Request.Headers + $Results.Add('MCP resource URIs and v2 tokens configured on the app registration. Run Save to Azure to apply the audiences and PRM scope.') + } catch { + $Results.Add(@{ + resultText = "Client saved, but MCP app configuration failed: $($_.Exception.Message)" + state = 'warning' + }) + } + } } if ($IPValidationErrors.Count -gt 0) { @@ -193,8 +207,18 @@ function Invoke-ExecApiClient { $FunctionAppName = $env:WEBSITE_SITE_NAME $AllClients = Get-CIPPAzDataTableEntity @Table -Filter 'Enabled eq true' | Where-Object { ![string]::IsNullOrEmpty($_.RowKey) } $ClientIds = $AllClients.RowKey + $McpClientIds = @($AllClients | Where-Object { $_.MCPAllowed -eq $true } | ForEach-Object { $_.RowKey }) try { - Set-CippApiAuth -RGName $RGName -FunctionAppName $FunctionAppName -TenantId $TenantId -ClientIds $ClientIds + Set-CippApiAuth -RGName $RGName -FunctionAppName $FunctionAppName -TenantId $TenantId -ClientIds $ClientIds -McpClientIds $McpClientIds + + # Advertise the MCP resource scope via App Service PRM so the Claude connector requests + # a scope that matches the resource app (clears AADSTS9010010). Cleared when no MCP clients. + if ($McpClientIds.Count -gt 0 -and $env:WEBSITE_HOSTNAME) { + $null = Update-CIPPAzFunctionAppSetting -Name $FunctionAppName -ResourceGroupName $RGName -AppSetting @{ 'WEBSITE_AUTH_PRM_DEFAULT_WITH_SCOPES' = "https://$($env:WEBSITE_HOSTNAME)/user_impersonation" } + } else { + $null = Update-CIPPAzFunctionAppSetting -Name $FunctionAppName -ResourceGroupName $RGName -AppSetting @{} -RemoveKeys @('WEBSITE_AUTH_PRM_DEFAULT_WITH_SCOPES') + } + $Body = @{ Results = 'API clients saved to Azure' } Write-LogMessage -headers $Request.Headers -API 'ExecApiClient' -message 'Saved API clients to Azure' -Sev 'Info' } catch { From ec2eb836f775584f85f06de10d8ad53d59e07334 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 3 Jun 2026 20:05:50 +0200 Subject: [PATCH 121/202] add logging to mcp reation. --- Modules/CIPPCore/Public/Authentication/Set-CippApiAuth.ps1 | 1 + .../HTTP Functions/CIPP/Settings/Invoke-ExecApiClient.ps1 | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Authentication/Set-CippApiAuth.ps1 b/Modules/CIPPCore/Public/Authentication/Set-CippApiAuth.ps1 index 884ff484bc37..4ce410cf21b4 100644 --- a/Modules/CIPPCore/Public/Authentication/Set-CippApiAuth.ps1 +++ b/Modules/CIPPCore/Public/Authentication/Set-CippApiAuth.ps1 @@ -19,6 +19,7 @@ function Set-CippApiAuth { Write-Information "[ApiAuth] SiteName=$SiteName, ResourceGroup=$ResourceGroup, SubscriptionId=$SubscriptionId" Write-Information "[ApiAuth] ClientIds to set: $($ClientIds -join ', ')" + Write-Information "[ApiAuth] MCP client IDs: $($McpClientIds -join ', ') | WEBSITE_HOSTNAME=$($env:WEBSITE_HOSTNAME)" if (-not $SiteName -or -not $ResourceGroup -or -not $SubscriptionId) { throw "[ApiAuth] Missing App Service env vars: WEBSITE_SITE_NAME=$SiteName, WEBSITE_RESOURCE_GROUP=$ResourceGroup, SubscriptionId=$SubscriptionId" diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecApiClient.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecApiClient.ps1 index 5175af03704b..4e54df51a9db 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecApiClient.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecApiClient.ps1 @@ -138,7 +138,7 @@ function Invoke-ExecApiClient { if ([bool]($Request.Body.MCPAllowed ?? $false)) { try { $null = Set-CIPPMCPClientApp -AppId $ClientId -Headers $Request.Headers - $Results.Add('MCP resource URIs and v2 tokens configured on the app registration. Run Save to Azure to apply the audiences and PRM scope.') + $Results.Add('MCP resource URIs and v2 tokens configured on the app registration. Run Save to Azure to apply the changes.') } catch { $Results.Add(@{ resultText = "Client saved, but MCP app configuration failed: $($_.Exception.Message)" @@ -207,7 +207,9 @@ function Invoke-ExecApiClient { $FunctionAppName = $env:WEBSITE_SITE_NAME $AllClients = Get-CIPPAzDataTableEntity @Table -Filter 'Enabled eq true' | Where-Object { ![string]::IsNullOrEmpty($_.RowKey) } $ClientIds = $AllClients.RowKey - $McpClientIds = @($AllClients | Where-Object { $_.MCPAllowed -eq $true } | ForEach-Object { $_.RowKey }) + # MCPAllowed can round-trip from table storage as a bool or a string; compare on string form. + $McpClientIds = @($AllClients | Where-Object { "$($_.MCPAllowed)" -eq 'True' } | ForEach-Object { $_.RowKey }) + Write-Information "[ExecApiClient] MCP clients resolved for audiences/scope: $($McpClientIds -join ', ')" try { Set-CippApiAuth -RGName $RGName -FunctionAppName $FunctionAppName -TenantId $TenantId -ClientIds $ClientIds -McpClientIds $McpClientIds From 49e8af8b9d9b867c9a44f12bfc536c02890e4af7 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Wed, 3 Jun 2026 19:24:52 +0200 Subject: [PATCH 122/202] feat: add Invoke-ExecSetCASMailbox function for CAS settings management --- .../Invoke-ExecSetCASMailbox.ps1 | 81 +++++++++++++++++++ .../Reports/Invoke-ListMailboxCAS.ps1 | 10 +-- 2 files changed, 82 insertions(+), 9 deletions(-) create mode 100644 Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecSetCASMailbox.ps1 diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecSetCASMailbox.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecSetCASMailbox.ps1 new file mode 100644 index 000000000000..953749f66f94 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecSetCASMailbox.ps1 @@ -0,0 +1,81 @@ +function Invoke-ExecSetCASMailbox { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Exchange.Mailbox.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + $TenantFilter = $Request.Body.tenantFilter + $Identity = $Request.Body.Identity + $DisplayName = $Request.Body.DisplayName ?? $Identity + + # The CAS protocols we allow toggling. Note SmtpClientAuthenticationDisabled is inverted: + # $true means SMTP client authentication is DISABLED for the mailbox. + $ValidProtocols = @( + 'OWAEnabled' + 'ECPEnabled' + 'IMAPEnabled' + 'POPEnabled' + 'MAPIEnabled' + 'EWSEnabled' + 'ActiveSyncEnabled' + 'SmtpClientAuthenticationDisabled' + ) + + # Build the cmdlet parameters from any valid protocol values supplied in the body. + $CmdParams = @{ Identity = $Identity } + foreach ($Protocol in $ValidProtocols) { + if ($null -ne $Request.Body.$Protocol) { + $CmdParams[$Protocol] = [System.Convert]::ToBoolean($Request.Body.$Protocol) + } + } + + # SMTP client authentication can only be turned off via this endpoint. Drop an enable + # attempt (SmtpClientAuthenticationDisabled = $false) but still apply the other protocols. + $Warnings = [System.Collections.Generic.List[string]]::new() + if ($CmdParams.ContainsKey('SmtpClientAuthenticationDisabled') -and $CmdParams['SmtpClientAuthenticationDisabled'] -eq $false) { + $null = $CmdParams.Remove('SmtpClientAuthenticationDisabled') + $Warnings.Add('SMTP Client Authentication can only be disabled, not enabled, and was left unchanged.') + } + + # Nothing left to apply: return the warning if we dropped one, otherwise a generic message. + if ($CmdParams.Keys.Count -le 1) { + $Results = $Warnings.Count -gt 0 ? ($Warnings -join ' ') : 'No CAS protocol settings were supplied.' + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message $Results -Sev 'Info' + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = @{ 'Results' = $Results } + }) + } + + # Human readable summary of the change(s) for logging and the API result. + $ChangeSummary = ($CmdParams.GetEnumerator() | Where-Object { $_.Key -ne 'Identity' } | ForEach-Object { + '{0} = {1}' -f $_.Key, $_.Value + }) -join ', ' + + try { + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Set-CASMailbox' -cmdParams $CmdParams + $Results = "Successfully set CAS settings for $DisplayName ($ChangeSummary)" + if ($Warnings.Count -gt 0) { + $Results = '{0}. {1}' -f $Results, ($Warnings -join ' ') + } + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message $Results -Sev Info + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Results = "Failed to set CAS settings for $DisplayName. Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message $Results -Sev Error -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::InternalServerError + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{ 'Results' = $Results } + }) +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListMailboxCAS.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListMailboxCAS.ps1 index 2e66a472ddab..b8a43c2667fa 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListMailboxCAS.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListMailboxCAS.ps1 @@ -10,15 +10,7 @@ function Invoke-ListMailboxCAS { # Interact with query parameters or the body of the request. $TenantFilter = $Request.Query.TenantFilter try { - $GraphRequest = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-CasMailbox' | Select-Object @{ Name = 'displayName'; Expression = { $_.'DisplayName' } }, - @{ Name = 'primarySmtpAddress'; Expression = { $_.'PrimarySMTPAddress' } }, - @{ Name = 'ecpenabled'; Expression = { $_.'ECPEnabled' } }, - @{ Name = 'owaenabled'; Expression = { $_.'OWAEnabled' } }, - @{ Name = 'imapenabled'; Expression = { $_.'IMAPEnabled' } }, - @{ Name = 'popenabled'; Expression = { $_.'POPEnabled' } }, - @{ Name = 'mapienabled'; Expression = { $_.'MAPIEnabled' } }, - @{ Name = 'ewsenabled'; Expression = { $_.'EWSEnabled' } }, - @{ Name = 'activesyncenabled'; Expression = { $_.'ActiveSyncEnabled' } } + $GraphRequest = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-CasMailbox' | Select-Object DisplayName, PrimarySmtpAddress, Guid, ECPEnabled, OWAEnabled, IMAPEnabled, POPEnabled, MAPIEnabled, EWSEnabled, ActiveSyncEnabled, SmtpClientAuthenticationDisabled $StatusCode = [HttpStatusCode]::OK } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message From 235063643757177c83cc380d14af2464f921f897 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 3 Jun 2026 20:38:08 +0200 Subject: [PATCH 123/202] add tot non-ng --- .../Public/Authentication/Set-CippApiAuth.ps1 | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/Modules/CIPPCore/Public/Authentication/Set-CippApiAuth.ps1 b/Modules/CIPPCore/Public/Authentication/Set-CippApiAuth.ps1 index 4ce410cf21b4..725ec3886cd8 100644 --- a/Modules/CIPPCore/Public/Authentication/Set-CippApiAuth.ps1 +++ b/Modules/CIPPCore/Public/Authentication/Set-CippApiAuth.ps1 @@ -116,10 +116,19 @@ function Set-CippApiAuth { Write-Information "AuthSettings: $($AuthSettings | ConvertTo-Json -Depth 10)" - # Set allowed audiences - $AllowedAudiences = foreach ($ClientId in $ClientIds) { - "api://$ClientId" + # Set allowed audiences (api://{id} for each, plus MCP resource URIs + bare appId for MCP clients) + $AudienceList = [System.Collections.Generic.List[string]]::new() + foreach ($ClientId in $ClientIds) { + $AudienceList.Add("api://$ClientId") } + if ($McpClientIds -and $env:WEBSITE_HOSTNAME) { + $AudienceList.Add("https://$($env:WEBSITE_HOSTNAME)") + $AudienceList.Add("https://$($env:WEBSITE_HOSTNAME)/api/ExecMcp") + foreach ($McpId in $McpClientIds) { + if (-not [string]::IsNullOrEmpty($McpId)) { $AudienceList.Add($McpId) } + } + } + $AllowedAudiences = @($AudienceList) if (!$AllowedAudiences) { $AllowedAudiences = @() } if (!$ClientIds) { $ClientIds = @() } From 72c277d77f26595a99f65af7726029e6ea87e362 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 3 Jun 2026 20:56:18 +0200 Subject: [PATCH 124/202] Allow MCP client --- .../Entrypoints/HTTP Functions/CIPP/MCP/Invoke-ExecMcp.ps1 | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/MCP/Invoke-ExecMcp.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/MCP/Invoke-ExecMcp.ps1 index ddb3a8e8dd27..60da65909eba 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/MCP/Invoke-ExecMcp.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/MCP/Invoke-ExecMcp.ps1 @@ -27,12 +27,9 @@ function Invoke-ExecMcp { }) } - # Per-client gate: the global 'MCPServer' feature flag lets this endpoint run at all (enforced - # upstream in New-CippCoreRequest); this narrows access to API clients explicitly flagged - # 'MCP Access Allowed'. A non-API-client caller, an unknown client, or one without the flag is denied. $CallerAppId = $Request.Headers.'x-ms-client-principal-name' $IsApiClient = $Request.Headers.'x-ms-client-principal-idp' -eq 'aad' -and $CallerAppId -match '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' - $McpAllowed = if ($IsApiClient) { [bool](Get-CippApiClient -AppId $CallerAppId).MCPAllowed } else { $false } + $McpAllowed = if ($IsApiClient) { [bool](Get-CippApiClient -AppId $CallerAppId).MCPAllowed } else { $true } if (-not $McpAllowed) { return ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::Forbidden From 7800b3cb14db994b1867a4f6f5bb1116e818e336 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 3 Jun 2026 21:22:27 +0200 Subject: [PATCH 125/202] Fix --- .../Public/Authentication/Test-CIPPAccess.ps1 | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 b/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 index 3e9832be532c..06c728084971 100644 --- a/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 +++ b/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 @@ -136,6 +136,19 @@ function Test-CIPPAccess { $swUserBranch = [System.Diagnostics.Stopwatch]::StartNew() $User = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Request.Headers.'x-ms-client-principal')) | ConvertFrom-Json + if ($User.claims -and [string]::IsNullOrWhiteSpace($User.userDetails)) { + $Claims = @($User.claims) + $Upn = ($Claims | Where-Object { $_.typ -in @('preferred_username', 'upn', 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn', 'email', 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress') } | Select-Object -First 1).val + if ([string]::IsNullOrWhiteSpace($Upn)) { $Upn = $Request.Headers.'x-ms-client-principal-name' } + $Oid = ($Claims | Where-Object { $_.typ -in @('http://schemas.microsoft.com/identity/claims/objectidentifier', 'oid') } | Select-Object -First 1).val + $User = [pscustomobject]@{ + identityProvider = 'aad' + userId = $Oid + userDetails = $Upn + userRoles = @('authenticated', 'anonymous') + } + } + # Check for roles granted via group membership if (($User.userRoles | Measure-Object).Count -eq 2 -and $User.userRoles -contains 'authenticated' -and $User.userRoles -contains 'anonymous') { $swResolveUserRoles = [System.Diagnostics.Stopwatch]::StartNew() @@ -145,6 +158,9 @@ function Test-CIPPAccess { } $swIPCheck = [System.Diagnostics.Stopwatch]::StartNew() + if (-not $User.userRoles) { + throw 'Access denied: unable to resolve roles for the authenticated principal.' + } $AllowedIPRanges = Get-CIPPRoleIPRanges -Roles $User.userRoles if ($AllowedIPRanges -notcontains 'Any') { From 77073ba369222e427671f8c75ba07facfbb08a2e Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 3 Jun 2026 23:49:53 +0200 Subject: [PATCH 126/202] role change --- .../HTTP Functions/New-CippCoreRequest.ps1 | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/New-CippCoreRequest.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/New-CippCoreRequest.ps1 index a457080639d2..985e6026d4a8 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/New-CippCoreRequest.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/New-CippCoreRequest.ps1 @@ -43,6 +43,24 @@ function New-CippCoreRequest { $FunctionName = 'Invoke-{0}' -f $Request.Params.CIPPEndpoint Write-Information "API Endpoint: $($Request.Params.CIPPEndpoint) | Frontend Version: $($Request.Headers.'X-CIPP-Version' ?? 'Not specified')" + # For now, while we're in read-only we force the role of the MCP API cred. + # When we remove the feature flag, in NG, we move this to use the users role/ident. + if ($Request.Params.CIPPEndpoint -eq 'ExecMcp' -and + $Request.Headers.'x-ms-client-principal' -and + $Request.Headers.'x-ms-client-principal-name' -notmatch '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$') { + try { + $McpPrincipal = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Request.Headers.'x-ms-client-principal')) | ConvertFrom-Json + $McpAppId = ($McpPrincipal.claims | Where-Object { $_.typ -in @('azp', 'appid') } | Select-Object -First 1).val + if ($McpAppId -and (Get-CippApiClient -AppId $McpAppId)) { + $Request.Headers | Add-Member -NotePropertyName 'x-ms-client-principal-name' -NotePropertyValue $McpAppId -Force + $Request.Headers | Add-Member -NotePropertyName 'x-ms-client-principal-idp' -NotePropertyValue 'aad' -Force + Write-Information "MCP request mapped to API client $McpAppId (running at the app's CIPP role)" + } + } catch { + Write-Information "MCP principal app resolution failed: $($_.Exception.Message)" + } + } + # Check if endpoint is disabled via feature flags $FeatureFlags = Get-CIPPFeatureFlag $DisabledEndpoint = $FeatureFlags | Where-Object { From bc6aee43b74cd0672d858b9d31effbda6283847a Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 3 Jun 2026 21:33:32 -0400 Subject: [PATCH 127/202] fix: quarantine deny action fixes https://github.com/KelvinTegelaar/CIPP/issues/5930 --- .../Invoke-ExecQuarantineManagement.ps1 | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 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 44f942384722..08f15c3e275e 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 @@ -14,6 +14,14 @@ function Invoke-ExecQuarantineManagement { $TenantFilter = $Request.Body.tenantFilter | Select-Object -First 1 $ActionType = $Request.Body.Type | Select-Object -First 1 $AllowSender = $Request.Body.AllowSender -eq $true + $RecipientAddresses = @($Request.Body.RecipientAddress | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) + $UserRecipients = @( + $RecipientAddresses | + ForEach-Object { $_ -split '[,;]' } | + ForEach-Object { $_.Trim() } | + Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | + Select-Object -Unique + ) $params = @{} if ($ActionType -eq 'Release') { @@ -27,11 +35,15 @@ function Invoke-ExecQuarantineManagement { } else { $params['ActionType'] = $ActionType if ($Request.Body.Identity -is [string]) { - $params['Identities'] = @($Request.Body.Identity) + $params['Identity'] = $Request.Body.Identity } else { $params['Identities'] = $Request.Body.Identity + # For -Identities, Exchange requires -Identity to be present, but ignores its value. + $params['Identity'] = '000' + } + if ($ActionType -eq 'Deny' -and $UserRecipients.Count -gt 0) { + $params['User'] = $UserRecipients } - $params['Identity'] = '000' } New-ExoRequest -tenantid $TenantFilter -cmdlet 'Release-QuarantineMessage' -cmdParams $params From 4288bd80f1d901e73c9054c887f8b9d3d2068da3 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 4 Jun 2026 16:04:01 +0800 Subject: [PATCH 128/202] exclude partner tenant --- .../Public/TenantGroups/Get-TenantGroups.ps1 | 15 ++++++++------- .../Update-CIPPDynamicTenantGroups.ps1 | 9 +++++++++ .../Settings/Invoke-ExecCreateDefaultGroups.ps1 | 5 ++++- .../CIPP/Settings/Invoke-ExecTenantGroup.ps1 | 4 ++++ 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/Modules/CIPPCore/Public/TenantGroups/Get-TenantGroups.ps1 b/Modules/CIPPCore/Public/TenantGroups/Get-TenantGroups.ps1 index 96f70c91f1db..1c978efd42e9 100644 --- a/Modules/CIPPCore/Public/TenantGroups/Get-TenantGroups.ps1 +++ b/Modules/CIPPCore/Public/TenantGroups/Get-TenantGroups.ps1 @@ -180,13 +180,14 @@ function Get-TenantGroups { } $Results.Add([PSCustomObject]@{ - Id = $Group.RowKey - Name = $Group.Name - Description = $Group.Description - GroupType = $Group.GroupType ?? 'static' - RuleLogic = $Group.RuleLogic ?? 'and' - DynamicRules = $Group.DynamicRules ? @($Group.DynamicRules | ConvertFrom-Json) : @() - Members = @($SortedMembers) + Id = $Group.RowKey + Name = $Group.Name + Description = $Group.Description + GroupType = $Group.GroupType ?? 'static' + RuleLogic = $Group.RuleLogic ?? 'and' + ExcludePartnerTenant = [bool]($Group.ExcludePartnerTenant) + DynamicRules = $Group.DynamicRules ? @($Group.DynamicRules | ConvertFrom-Json) : @() + Members = @($SortedMembers) }) } diff --git a/Modules/CIPPCore/Public/TenantGroups/Update-CIPPDynamicTenantGroups.ps1 b/Modules/CIPPCore/Public/TenantGroups/Update-CIPPDynamicTenantGroups.ps1 index 0c4c7e343f0a..1ce08749fcc0 100644 --- a/Modules/CIPPCore/Public/TenantGroups/Update-CIPPDynamicTenantGroups.ps1 +++ b/Modules/CIPPCore/Public/TenantGroups/Update-CIPPDynamicTenantGroups.ps1 @@ -153,6 +153,15 @@ function Update-CIPPDynamicTenantGroups { $ScriptBlock = [ScriptBlock]::Create($WhereString) $MatchingTenants = $TenantObj | Where-Object $ScriptBlock + if ($Group.ExcludePartnerTenant -eq $true -and $env:TenantID) { + $BeforeCount = ($MatchingTenants | Measure-Object).Count + $MatchingTenants = @($MatchingTenants | Where-Object { $_.customerId -ne $env:TenantID }) + $RemovedCount = $BeforeCount - ($MatchingTenants | Measure-Object).Count + if ($RemovedCount -gt 0) { + Write-Information "ExcludePartnerTenant is enabled for group '$($Group.Name)'; removed $RemovedCount partner tenant from results" + } + } + Write-Information "Found $($MatchingTenants.Count) matching tenants for group '$($Group.Name)'" $CurrentMembers = @($AllGroupMembers | Where-Object { $_.GroupId -eq $Group.RowKey }) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCreateDefaultGroups.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCreateDefaultGroups.ps1 index 4fc1b2f5575c..089d3cf39ba5 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCreateDefaultGroups.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCreateDefaultGroups.ps1 @@ -16,7 +16,7 @@ function Invoke-ExecCreateDefaultGroups { $Table = Get-CippTable -tablename 'TenantGroups' $Results = [System.Collections.Generic.List[object]]::new() $ExistingGroups = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq 'TenantGroup' and Type eq 'dynamic'" - $DefaultGroups = '[{"PartitionKey":"TenantGroup","RowKey":"369d985e-0fba-48f9-844f-9f793b10a12c","Description":"This group does not have a license for intune, nor a license for Entra ID Premium","Description@type":null,"DynamicRules":"[{\"property\":\"availableServicePlan\",\"operator\":\"notIn\",\"value\":[{\"label\":\"Microsoft Intune\",\"value\":\"INTUNE_A\",\"id\":\"c1ec4a95-1f05-45b3-a911-aa3fa01094f5\"}]},{\"property\":\"availableServicePlan\",\"operator\":\"notIn\",\"value\":[{\"label\":\"Microsoft Entra ID P1\",\"value\":\"AAD_PREMIUM\",\"id\":\"41781fb2-bc02-4b7c-bd55-b576c07bb09d\"}]}]","DynamicRules@type":null,"GroupType":"dynamic","GroupType@type":null,"RuleLogic":"and","RuleLogic@type":null,"Name":"Not Intune and Entra Premium Capable","Name@type":null},{"PartitionKey":"TenantGroup","RowKey":"4dbca08b-7dc5-4e0f-bc25-14a90c8e0941","Description":"This group has atleast one Business Premium License available","DynamicRules":"{\"property\":\"availableLicense\",\"operator\":\"in\",\"value\":[{\"label\":\"Microsoft 365 Business Premium\",\"value\":\"SPB\",\"guid\":\"cbdc14ab-d96c-4c30-b9f4-6ada7cdc1d46\"},{\"label\":\"Microsoft 365 Business Premium (no Teams)\",\"value\":\"Microsoft_365_ Business_ Premium_(no Teams)\",\"guid\":\"00e1ec7b-e4a3-40d1-9441-b69b597ab222\"},{\"label\":\"Microsoft 365 Business Premium Donation\",\"value\":\"Microsoft_365_Business_Premium_Donation_(Non_Profit_Pricing)\",\"guid\":\"24c35284-d768-4e53-84d9-b7ae73dddf69\"},{\"label\":\"Microsoft 365 Business Premium EEA (no Teams)\",\"value\":\"Office_365_w/o_Teams_Bundle_Business_Premium\",\"guid\":\"a3f586b6-8cce-4d9b-99d6-55238397f77a\"}]}","GroupType":"dynamic","Name":"Business Premium License available","RuleLogic":"or"},{"PartitionKey":"TenantGroup","RowKey":"703c0e69-84a8-4dcf-a1c2-4986d2ccc850","Description":"This group does have a license for Entra Premium but does not have a license for Intune","Description@type":null,"DynamicRules":"[{\"property\":\"availableServicePlan\",\"operator\":\"in\",\"value\":[{\"label\":\"Microsoft Entra ID P1\",\"value\":\"AAD_PREMIUM\",\"id\":\"41781fb2-bc02-4b7c-bd55-b576c07bb09d\"}]},{\"property\":\"availableServicePlan\",\"operator\":\"notIn\",\"value\":[{\"label\":\"Microsoft Intune\",\"value\":\"INTUNE_A\",\"id\":\"c1ec4a95-1f05-45b3-a911-aa3fa01094f5\"}]}]","DynamicRules@type":null,"GroupType":"dynamic","GroupType@type":null,"RuleLogic":"and","RuleLogic@type":null,"Name":"Entra Premium Capable, Not Intune Capable","Name@type":null},{"PartitionKey":"TenantGroup","RowKey":"c1dadbc0-f0b4-448c-a2e6-e1938ba102e0","Description":"This group has Intune and Entra ID Premium available","Description@type":null,"DynamicRules":"{\"property\":\"availableServicePlan\",\"operator\":\"in\",\"value\":[{\"label\":\"Microsoft Intune\",\"value\":\"INTUNE_A\"},{\"label\":\"Microsoft Entra ID P1\",\"value\":\"AAD_PREMIUM\"}]}","DynamicRules@type":null,"GroupType":"dynamic","GroupType@type":null,"RuleLogic":"and","RuleLogic@type":null,"Name":"Entra ID Premium and Intune Capable","Name@type":null}]' | ConvertFrom-Json + $DefaultGroups = '[{"PartitionKey":"TenantGroup","RowKey":"369d985e-0fba-48f9-844f-9f793b10a12c","Description":"This group does not have a license for intune, nor a license for Entra ID Premium","Description@type":null,"DynamicRules":"[{\"property\":\"availableServicePlan\",\"operator\":\"notIn\",\"value\":[{\"label\":\"Microsoft Intune\",\"value\":\"INTUNE_A\",\"id\":\"c1ec4a95-1f05-45b3-a911-aa3fa01094f5\"}]},{\"property\":\"availableServicePlan\",\"operator\":\"notIn\",\"value\":[{\"label\":\"Microsoft Entra ID P1\",\"value\":\"AAD_PREMIUM\",\"id\":\"41781fb2-bc02-4b7c-bd55-b576c07bb09d\"}]}]","DynamicRules@type":null,"GroupType":"dynamic","GroupType@type":null,"RuleLogic":"and","RuleLogic@type":null,"Name":"Not Intune and Entra Premium Capable","Name@type":null},{"PartitionKey":"TenantGroup","RowKey":"4dbca08b-7dc5-4e0f-bc25-14a90c8e0941","Description":"This group has atleast one Business Premium License available","DynamicRules":"{\"property\":\"availableLicense\",\"operator\":\"in\",\"value\":[{\"label\":\"Microsoft 365 Business Premium\",\"value\":\"SPB\",\"guid\":\"cbdc14ab-d96c-4c30-b9f4-6ada7cdc1d46\"},{\"label\":\"Microsoft 365 Business Premium (no Teams)\",\"value\":\"Microsoft_365_ Business_ Premium_(no Teams)\",\"guid\":\"00e1ec7b-e4a3-40d1-9441-b69b597ab222\"},{\"label\":\"Microsoft 365 Business Premium Donation\",\"value\":\"Microsoft_365_Business_Premium_Donation_(Non_Profit_Pricing)\",\"guid\":\"24c35284-d768-4e53-84d9-b7ae73dddf69\"},{\"label\":\"Microsoft 365 Business Premium EEA (no Teams)\",\"value\":\"Office_365_w/o_Teams_Bundle_Business_Premium\",\"guid\":\"a3f586b6-8cce-4d9b-99d6-55238397f77a\"}]}","GroupType":"dynamic","Name":"Business Premium License available","RuleLogic":"or"},{"PartitionKey":"TenantGroup","RowKey":"703c0e69-84a8-4dcf-a1c2-4986d2ccc850","Description":"This group does have a license for Entra Premium but does not have a license for Intune","Description@type":null,"DynamicRules":"[{\"property\":\"availableServicePlan\",\"operator\":\"in\",\"value\":[{\"label\":\"Microsoft Entra ID P1\",\"value\":\"AAD_PREMIUM\",\"id\":\"41781fb2-bc02-4b7c-bd55-b576c07bb09d\"}]},{\"property\":\"availableServicePlan\",\"operator\":\"notIn\",\"value\":[{\"label\":\"Microsoft Intune\",\"value\":\"INTUNE_A\",\"id\":\"c1ec4a95-1f05-45b3-a911-aa3fa01094f5\"}]}]","DynamicRules@type":null,"GroupType":"dynamic","GroupType@type":null,"RuleLogic":"and","RuleLogic@type":null,"Name":"Entra Premium Capable, Not Intune Capable","Name@type":null},{"PartitionKey":"TenantGroup","RowKey":"c1dadbc0-f0b4-448c-a2e6-e1938ba102e0","Description":"This group has Intune and Entra ID Premium available","Description@type":null,"DynamicRules":"{\"property\":\"availableServicePlan\",\"operator\":\"in\",\"value\":[{\"label\":\"Microsoft Intune\",\"value\":\"INTUNE_A\"},{\"label\":\"Microsoft Entra ID P1\",\"value\":\"AAD_PREMIUM\"}]}","DynamicRules@type":null,"GroupType":"dynamic","GroupType@type":null,"RuleLogic":"and","RuleLogic@type":null,"Name":"Entra ID Premium and Intune Capable","Name@type":null},{"PartitionKey":"TenantGroup","RowKey":"a8e3c4d2-7f0b-4f8e-9b91-0d4f1c5e2a90","Description":"All tenants managed via GDAP or direct relationship, excluding the partner tenant itself","DynamicRules":"[{\"property\":\"delegatedAccessStatus\",\"operator\":\"eq\",\"value\":{\"label\":\"Granular Delegated Admin Privileges\",\"value\":\"granularDelegatedAdminPrivileges\"}},{\"property\":\"delegatedAccessStatus\",\"operator\":\"eq\",\"value\":{\"label\":\"Direct Tenant\",\"value\":\"directTenant\"}}]","GroupType":"dynamic","Name":"All Tenants (Excluding Partner)","RuleLogic":"or","ExcludePartnerTenant":true}]' | ConvertFrom-Json foreach ($Group in $DefaultGroups) { @@ -38,6 +38,9 @@ function Invoke-ExecCreateDefaultGroups { DynamicRules = $Group.DynamicRules RuleLogic = $Group.RuleLogic } + if ($null -ne $Group.ExcludePartnerTenant) { + $GroupEntity.ExcludePartnerTenant = [bool]$Group.ExcludePartnerTenant + } Add-CIPPAzDataTableEntity @Table -Entity $GroupEntity -Force $Results.Add(@{ diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecTenantGroup.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecTenantGroup.ps1 index a0d869ade67f..0467a155e4ba 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecTenantGroup.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecTenantGroup.ps1 @@ -22,6 +22,7 @@ function Invoke-ExecTenantGroup { $groupType = $Request.Body.groupType ?? 'static' $dynamicRules = $Request.Body.dynamicRules $ruleLogic = $Request.Body.ruleLogic ?? 'and' + $excludePartnerTenant = [bool]($Request.Body.excludePartnerTenant) # Validate dynamic rules to prevent code injection if ($groupType -eq 'dynamic' -and $dynamicRules) { @@ -67,8 +68,10 @@ function Invoke-ExecTenantGroup { if ($groupType -eq 'dynamic' -and $dynamicRules) { $GroupEntity | Add-Member -NotePropertyName 'DynamicRules' -NotePropertyValue "$($dynamicRules | ConvertTo-Json -Depth 100 -Compress)" -Force $GroupEntity | Add-Member -NotePropertyName 'RuleLogic' -NotePropertyValue $ruleLogic -Force + $GroupEntity | Add-Member -NotePropertyName 'ExcludePartnerTenant' -NotePropertyValue $excludePartnerTenant -Force } else { $GroupEntity | Add-Member -NotePropertyName 'RuleLogic' -NotePropertyValue $null -Force + $GroupEntity | Add-Member -NotePropertyName 'ExcludePartnerTenant' -NotePropertyValue $false -Force } Add-CIPPAzDataTableEntity @Table -Entity $GroupEntity -Force } else { @@ -82,6 +85,7 @@ function Invoke-ExecTenantGroup { if ($groupType -eq 'dynamic' -and $dynamicRules) { $GroupEntity.DynamicRules = "$($dynamicRules | ConvertTo-Json -Depth 100 -Compress)" $GroupEntity.RuleLogic = $ruleLogic + $GroupEntity.ExcludePartnerTenant = $excludePartnerTenant } Add-CIPPAzDataTableEntity @Table -Entity $GroupEntity -Force } From adba8fa7febc74db4fe15f8e06941a9ce3d58540 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 4 Jun 2026 12:21:33 +0200 Subject: [PATCH 129/202] add excludeFromAlert to licenses. --- .../Alerts/Get-CIPPAlertExpiringLicenses.ps1 | 2 +- .../Alerts/Get-CIPPAlertOverusedLicenses.ps1 | 30 +++++++------------ .../Alerts/Get-CIPPAlertUnusedLicenses.ps1 | 28 +++++++---------- .../Public/Get-CIPPLicenseOverview.ps1 | 15 ++++++++-- .../Settings/Invoke-ExecExcludeLicenses.ps1 | 14 +++++++++ .../Settings/Invoke-ListExcludedLicenses.ps1 | 10 ++++++- .../Sync-CippExtensionData.ps1 | 6 +++- .../Gradient/New-GradientServiceSyncRun.ps1 | 6 +++- 8 files changed, 68 insertions(+), 43 deletions(-) diff --git a/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertExpiringLicenses.ps1 b/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertExpiringLicenses.ps1 index 0b3523a2fd53..15fcfdf3009b 100644 --- a/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertExpiringLicenses.ps1 +++ b/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertExpiringLicenses.ps1 @@ -24,7 +24,7 @@ function Get-CIPPAlertExpiringLicenses { } $AlertData = @( - Get-CIPPLicenseOverview -TenantFilter $TenantFilter | ForEach-Object { + Get-CIPPLicenseOverview -TenantFilter $TenantFilter -AlertMode | ForEach-Object { $UnassignedCount = [int]$_.CountAvailable diff --git a/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertOverusedLicenses.ps1 b/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertOverusedLicenses.ps1 index a5fcd3d85394..527f9d46e3bd 100644 --- a/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertOverusedLicenses.ps1 +++ b/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertOverusedLicenses.ps1 @@ -13,31 +13,23 @@ function Get-CIPPAlertOverusedLicenses { try { - $LicenseTable = Get-CIPPTable -TableName ExcludedLicenses - $ExcludedSkuList = Get-CIPPAzDataTableEntity @LicenseTable - $AlertData = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/subscribedSkus' -tenantid $TenantFilter | ForEach-Object { - $skuid = $_ - foreach ($sku in $skuid) { - if ($sku.skuId -in $ExcludedSkuList.GUID) { continue } - $PrettyName = Convert-SKUname -SkuID $sku.skuId - if (!$PrettyName) { $PrettyName = $sku.skuPartNumber } - if ($sku.prepaidUnits.enabled - $sku.consumedUnits -lt 0) { - [PSCustomObject]@{ - Message = "$PrettyName has Overused licenses. Using $($sku.consumedUnits) of $($sku.prepaidUnits.enabled)." - LicenseName = $PrettyName - SkuId = $sku.skuId - SkuPartNumber = $sku.skuPartNumber - ConsumedUnits = $sku.consumedUnits - EnabledUnits = $sku.prepaidUnits.enabled - Tenant = $TenantFilter - } + $AlertData = Get-CIPPLicenseOverview -TenantFilter $TenantFilter -AlertMode | ForEach-Object { + if ([int]$_.CountAvailable -lt 0) { + [PSCustomObject]@{ + Message = "$($_.License) has Overused licenses. Using $($_.CountUsed) of $($_.TotalLicenses)." + LicenseName = $_.License + SkuId = $_.skuId + SkuPartNumber = $_.skuPartNumber + ConsumedUnits = $_.CountUsed + EnabledUnits = $_.TotalLicenses + Tenant = $TenantFilter } } } + if ($AlertData) { Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData } - } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Alerts' -tenant $TenantFilter -message "Overused Licenses Alert Error occurred: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage diff --git a/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertUnusedLicenses.ps1 b/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertUnusedLicenses.ps1 index c24f8ff4bc5f..98f958da3a88 100644 --- a/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertUnusedLicenses.ps1 +++ b/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertUnusedLicenses.ps1 @@ -12,24 +12,16 @@ function Get-CIPPAlertUnusedLicenses { ) try { - $LicenseTable = Get-CIPPTable -TableName ExcludedLicenses - $ExcludedSkuList = Get-CIPPAzDataTableEntity @LicenseTable - $AlertData = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/subscribedSkus' -tenantid $TenantFilter | ForEach-Object { - $SkuId = $_ - foreach ($sku in $SkuId) { - if ($sku.skuId -in $ExcludedSkuList.GUID) { continue } - $PrettyName = Convert-SKUname -SkuID $sku.skuId - if (!$PrettyName) { $PrettyName = $sku.skuPartNumber } - if ($sku.prepaidUnits.enabled - $sku.consumedUnits -gt 0) { - [PSCustomObject]@{ - Message = "$PrettyName has unused licenses. Using $($sku.consumedUnits) of $($sku.prepaidUnits.enabled)." - LicenseName = $PrettyName - SkuId = $sku.skuId - SkuPartNumber = $sku.skuPartNumber - ConsumedUnits = $sku.consumedUnits - EnabledUnits = $sku.prepaidUnits.enabled - Tenant = $TenantFilter - } + $AlertData = Get-CIPPLicenseOverview -TenantFilter $TenantFilter -AlertMode | ForEach-Object { + if ([int]$_.CountAvailable -gt 0) { + [PSCustomObject]@{ + Message = "$($_.License) has unused licenses. Using $($_.CountUsed) of $($_.TotalLicenses)." + LicenseName = $_.License + SkuId = $_.skuId + SkuPartNumber = $_.skuPartNumber + ConsumedUnits = $_.CountUsed + EnabledUnits = $_.TotalLicenses + Tenant = $TenantFilter } } } diff --git a/Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 b/Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 index 1046ac91ac51..4260633df330 100644 --- a/Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 @@ -4,7 +4,8 @@ function Get-CIPPLicenseOverview { param ( $TenantFilter, $APIName = 'Get License Overview', - $Headers + $Headers, + [switch]$AlertMode ) $Requests = @( @@ -62,6 +63,16 @@ function Get-CIPPLicenseOverview { $ExcludedSkuList = Get-CIPPAzDataTableEntity @LicenseTable } + # In AlertMode, exclude all licenses in the table (both ExcludedEverywhere and alert-only) + # In normal mode, only exclude licenses where ExcludedEverywhere is true (or null for backward compat) + if ($AlertMode) { + $EffectiveExcludedGuids = @($ExcludedSkuList.GUID) + } else { + $EffectiveExcludedGuids = @($ExcludedSkuList | Where-Object { + $null -eq $_.ExcludedEverywhere -or $_.ExcludedEverywhere -eq $true + } | ForEach-Object { $_.GUID }) + } + $AllLicensedUsers = @(($Results | Where-Object { $_.id -eq 'licensedUsers' }).body.value) | Sort-Object -Property displayName $UsersBySku = @{} foreach ($User in $AllLicensedUsers) { @@ -109,7 +120,7 @@ function Get-CIPPLicenseOverview { $GraphRequest = foreach ($singleReq in $RawGraphRequest) { $skuId = $singleReq.Licenses foreach ($sku in $skuId) { - if ($sku.skuId -in $ExcludedSkuList.GUID) { continue } + if ($sku.skuId -in $EffectiveExcludedGuids) { continue } $PrettyNameAdmin = $AdminPortalLicenses | Where-Object { $_.aadSkuId -eq $sku.skuId } | Select-Object -ExpandProperty displayName -First 1 $PrettyNameCSV = ($ConvertTable | Where-Object { $_.guid -eq $sku.skuid }).'Product_Display_Name' | Select-Object -Last 1 $PrettyName = $PrettyNameAdmin ?? $PrettyNameCSV ?? $sku.skuPartNumber diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExcludeLicenses.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExcludeLicenses.ps1 index 0c44628dbac1..40a2a968a690 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExcludeLicenses.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExcludeLicenses.ps1 @@ -25,11 +25,25 @@ function Invoke-ExecExcludeLicenses { RowKey = $GUID 'GUID' = $GUID 'Product_Display_Name' = $DisplayName + 'ExcludedEverywhere' = $true } Add-CIPPAzDataTableEntity @Table -Entity $AddObject -Force $Result = "Success. Added $DisplayName($GUID) to the excluded licenses list." Write-LogMessage -API $APIName -headers $Headers -message $Result -Sev 'Info' + } + 'AlertOnly' { + $AddObject = @{ + PartitionKey = 'License' + RowKey = $GUID + 'GUID' = $GUID + 'Product_Display_Name' = $DisplayName + 'ExcludedEverywhere' = $false + } + Add-CIPPAzDataTableEntity @Table -Entity $AddObject -Force + $Result = "Success. $DisplayName($GUID) will now only be excluded from alerts." + Write-LogMessage -API $APIName -headers $Headers -message $Result -Sev 'Info' + } 'RemoveExclusion' { $Filter = "RowKey eq '{0}' and PartitionKey eq 'License'" -f $GUID diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListExcludedLicenses.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListExcludedLicenses.ps1 index 764cbdc2ce79..06ddff582e2a 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListExcludedLicenses.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListExcludedLicenses.ps1 @@ -22,7 +22,15 @@ function Invoke-ListExcludedLicenses { $Rows = Get-CIPPAzDataTableEntity @Table } - $Results = @($Rows) + $Results = @($Rows | ForEach-Object { + # Normalize ExcludedEverywhere for legacy rows that don't have the property + if ($null -eq $_.ExcludedEverywhere) { + $_ | Add-Member -NotePropertyName 'ExcludedEverywhere' -NotePropertyValue $true -Force + } + $ExclusionType = if ($_.ExcludedEverywhere -eq $true) { 'Excluded Everywhere' } else { 'Excluded from Alerts Only' } + $_ | Add-Member -NotePropertyName 'ExclusionType' -NotePropertyValue $ExclusionType -Force + $_ + }) } catch { $ErrorMessage = Get-CippException -Exception $_ $StatusCode = [HttpStatusCode]::InternalServerError diff --git a/Modules/CippExtensions/Public/Extension Functions/Sync-CippExtensionData.ps1 b/Modules/CippExtensions/Public/Extension Functions/Sync-CippExtensionData.ps1 index 5de6978331df..fa0ae80d764b 100644 --- a/Modules/CippExtensions/Public/Extension Functions/Sync-CippExtensionData.ps1 +++ b/Modules/CippExtensions/Public/Extension Functions/Sync-CippExtensionData.ps1 @@ -236,7 +236,11 @@ function Sync-CippExtensionData { $LicenseTable = Get-CIPPTable -TableName ExcludedLicenses $ExcludedSkuList = Get-CIPPAzDataTableEntity @LicenseTable if ($ExcludedSkuList) { - $Data = $Data | Where-Object { $_.skuId -notin $ExcludedSkuList.GUID } + # Only exclude licenses marked as ExcludedEverywhere (not alert-only exclusions) + $ExcludedEverywhereGuids = @($ExcludedSkuList | Where-Object { + $null -eq $_.ExcludedEverywhere -or $_.ExcludedEverywhere -eq $true + } | ForEach-Object { $_.GUID }) + $Data = $Data | Where-Object { $_.skuId -notin $ExcludedEverywhereGuids } } } diff --git a/Modules/CippExtensions/Public/Gradient/New-GradientServiceSyncRun.ps1 b/Modules/CippExtensions/Public/Gradient/New-GradientServiceSyncRun.ps1 index f56d504d8a35..15c1c4b74b4e 100644 --- a/Modules/CippExtensions/Public/Gradient/New-GradientServiceSyncRun.ps1 +++ b/Modules/CippExtensions/Public/Gradient/New-GradientServiceSyncRun.ps1 @@ -30,6 +30,10 @@ function New-GradientServiceSyncRun { $Table = Get-CIPPTable -TableName cachelicenses $LicenseTable = Get-CIPPTable -TableName ExcludedLicenses $ExcludedSkuList = Get-CIPPAzDataTableEntity @LicenseTable + # Only exclude licenses marked as ExcludedEverywhere (not alert-only exclusions) + $ExcludedEverywhereRowKeys = @($ExcludedSkuList | Where-Object { + $null -eq $_.ExcludedEverywhere -or $_.ExcludedEverywhere -eq $true + } | ForEach-Object { $_.RowKey }) $RawGraphRequest = $Tenants | ForEach-Object -Parallel { $domainName = $_.defaultDomainName @@ -37,7 +41,7 @@ function New-GradientServiceSyncRun { Import-Module (Join-Path $env:CIPPRootPath 'Modules\CIPPCore') Write-Host "Doing $domainName" try { - $Licrequest = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/subscribedSkus' -tenantid $_.defaultDomainName -ErrorAction Stop | Where-Object -Property skuId -NotIn $ExcludedSkuList.RowKey + $Licrequest = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/subscribedSkus' -tenantid $_.defaultDomainName -ErrorAction Stop | Where-Object -Property skuId -NotIn $using:ExcludedEverywhereRowKeys [PSCustomObject]@{ Tenant = $domainName Licenses = $Licrequest From 2b3a7bb50bc8c42c79dd9147baa46ef9e2e2ae1f Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 4 Jun 2026 12:40:42 +0200 Subject: [PATCH 130/202] resolves #6096 --- .../Identity/Administration/Users/Invoke-EditUser.ps1 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 417b90b22cc6..e5f9f1fc482c 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 @@ -65,7 +65,9 @@ function Invoke-EditUser { 'forceChangePasswordNextSignIn' = [bool]$UserObj.MustChangePass } } | ForEach-Object { - $NonEmptyProperties = $_.PSObject.Properties | Select-Object -ExpandProperty Name + $NonEmptyProperties = $_.PSObject.Properties | + Where-Object { -not [string]::IsNullOrWhiteSpace($_.Value) } | + Select-Object -ExpandProperty Name $_ | Select-Object -Property $NonEmptyProperties } if ($UserObj.defaultAttributes) { From 53db433cdee8008bd93188053151e692abdba4e0 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 4 Jun 2026 13:49:57 +0200 Subject: [PATCH 131/202] better descriptions --- .../Administration/Invoke-ListCalendarPermissions.ps1 | 2 ++ .../Email-Exchange/Administration/Invoke-ListHVEAccounts.ps1 | 2 ++ .../Email-Exchange/Administration/Invoke-ListMailboxRules.ps1 | 2 ++ .../Email-Exchange/Administration/Invoke-ListMailboxes.ps1 | 2 ++ .../Administration/Invoke-ListmailboxPermissions.ps1 | 2 ++ .../Email-Exchange/Reports/Invoke-ListMailboxForwarding.ps1 | 2 ++ .../HTTP Functions/Endpoint/Applications/Invoke-ListApps.ps1 | 2 ++ .../Endpoint/MEM/Invoke-ListAppProtectionPolicies.ps1 | 2 ++ .../Endpoint/MEM/Invoke-ListAssignmentFilters.ps1 | 2 ++ .../Endpoint/MEM/Invoke-ListCompliancePolicies.ps1 | 2 ++ .../HTTP Functions/Endpoint/MEM/Invoke-ListIntunePolicy.ps1 | 2 ++ .../Endpoint/MEM/Invoke-ListIntuneReusableSettings.ps1 | 2 ++ .../HTTP Functions/Endpoint/MEM/Invoke-ListIntuneScript.ps1 | 2 ++ .../Identity/Administration/Groups/Invoke-ListGroups.ps1 | 2 ++ .../HTTP Functions/Identity/Reports/Invoke-ListMFAUsers.ps1 | 2 ++ .../HTTP Functions/Security/Invoke-ListMDEOnboarding.ps1 | 2 ++ .../HTTP Functions/Teams-Sharepoint/Invoke-ListSites.ps1 | 2 ++ .../HTTP Functions/Teams-Sharepoint/Invoke-ListTeams.ps1 | 2 ++ .../Teams-Sharepoint/Invoke-ListTeamsActivity.ps1 | 2 ++ .../HTTP Functions/Teams-Sharepoint/Invoke-ListTeamsVoice.ps1 | 2 ++ .../HTTP Functions/Tenant/Reports/Invoke-ListOAuthApps.ps1 | 2 ++ 21 files changed, 42 insertions(+) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListCalendarPermissions.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListCalendarPermissions.ps1 index 902909f7cd64..93171cef4ef7 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListCalendarPermissions.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListCalendarPermissions.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListCalendarPermissions { Entrypoint .ROLE Exchange.Mailbox.Read + .DESCRIPTION + Lists calendar permissions for mailboxes in a tenant. Supports UseReportDB=true query parameter to retrieve cached data from the reporting database for significantly better performance, especially when querying AllTenants. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListHVEAccounts.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListHVEAccounts.ps1 index bdabf842f2b2..98cc5d49ff4b 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListHVEAccounts.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListHVEAccounts.ps1 @@ -4,6 +4,8 @@ function Invoke-ListHVEAccounts { Entrypoint .ROLE Exchange.Mailbox.Read + .DESCRIPTION + Lists High Volume Email (HVE) accounts and billing policies for a tenant. Supports UseReportDB=true query parameter to retrieve cached data from the reporting database for significantly better performance, especially when querying AllTenants. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxRules.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxRules.ps1 index 7dc29cbea73c..f15ac1f6c745 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxRules.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxRules.ps1 @@ -4,6 +4,8 @@ function Invoke-ListMailboxRules { Entrypoint .ROLE Exchange.Mailbox.Read + .DESCRIPTION + Lists inbox rules configured on mailboxes in a tenant. Supports UseReportDB=true query parameter to retrieve cached data from the reporting database for significantly better performance, especially when querying AllTenants. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxes.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxes.ps1 index c66c5477f9ee..7a9c854271ca 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxes.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxes.ps1 @@ -4,6 +4,8 @@ function Invoke-ListMailboxes { Entrypoint .ROLE Exchange.Mailbox.Read + .DESCRIPTION + Lists Exchange Online mailboxes for a tenant. Supports UseReportDB=true query parameter to retrieve cached data from the reporting database for significantly better performance, especially when querying AllTenants. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListmailboxPermissions.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListmailboxPermissions.ps1 index a569ed418d5e..37468a9c1b20 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListmailboxPermissions.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListmailboxPermissions.ps1 @@ -4,6 +4,8 @@ function Invoke-ListmailboxPermissions { Entrypoint .ROLE Exchange.Mailbox.Read + .DESCRIPTION + Lists mailbox permissions (Full Access, Send As, Send on Behalf) for a tenant. Supports UseReportDB=true query parameter to retrieve cached data from the reporting database for significantly better performance, especially when querying AllTenants. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListMailboxForwarding.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListMailboxForwarding.ps1 index 2eae2f67dd97..fa050769541a 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListMailboxForwarding.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListMailboxForwarding.ps1 @@ -4,6 +4,8 @@ function Invoke-ListMailboxForwarding { Entrypoint .ROLE Exchange.Mailbox.Read + .DESCRIPTION + Lists mailbox forwarding rules and configurations for a tenant. Supports UseReportDB=true query parameter to retrieve cached data from the reporting database for significantly better performance, especially when querying AllTenants. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-ListApps.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-ListApps.ps1 index 97e5db676935..f2d0dc06d28c 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-ListApps.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-ListApps.ps1 @@ -4,6 +4,8 @@ function Invoke-ListApps { Entrypoint .ROLE Endpoint.Application.Read + .DESCRIPTION + Lists Intune managed applications for a tenant. Supports UseReportDB=true query parameter to retrieve cached data from the reporting database for significantly better performance, especially when querying AllTenants. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListAppProtectionPolicies.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListAppProtectionPolicies.ps1 index f37004d07c34..698f5efa376f 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListAppProtectionPolicies.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListAppProtectionPolicies.ps1 @@ -4,6 +4,8 @@ Entrypoint .ROLE Endpoint.MEM.Read + .DESCRIPTION + Lists Intune app protection policies for a tenant. Supports UseReportDB=true query parameter to retrieve cached data from the reporting database for significantly better performance, especially when querying AllTenants. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListAssignmentFilters.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListAssignmentFilters.ps1 index 0afd214ce600..bc8ae0901626 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListAssignmentFilters.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListAssignmentFilters.ps1 @@ -4,6 +4,8 @@ function Invoke-ListAssignmentFilters { Entrypoint .ROLE Endpoint.MEM.Read + .DESCRIPTION + Lists Intune assignment filters for a tenant. Supports UseReportDB=true query parameter to retrieve cached data from the reporting database for significantly better performance, especially when querying AllTenants. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListCompliancePolicies.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListCompliancePolicies.ps1 index a27c227c299f..01b088d2b37a 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListCompliancePolicies.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListCompliancePolicies.ps1 @@ -4,6 +4,8 @@ Entrypoint .ROLE Endpoint.MEM.Read + .DESCRIPTION + Lists Intune device compliance policies for a tenant. Supports UseReportDB=true query parameter to retrieve cached data from the reporting database for significantly better performance, especially when querying AllTenants. #> [CmdletBinding()] param($Request, $TriggerMetadata) 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 8461ef571cdf..0354734b2323 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 @@ -5,6 +5,8 @@ function Invoke-ListIntunePolicy { Entrypoint .ROLE Endpoint.MEM.Read + .DESCRIPTION + Lists Intune device configuration policies for a tenant. Supports UseReportDB=true query parameter to retrieve cached data from the reporting database for significantly better performance, especially when querying AllTenants. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneReusableSettings.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneReusableSettings.ps1 index d73aa66f3079..d7d1973b09ee 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneReusableSettings.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneReusableSettings.ps1 @@ -4,6 +4,8 @@ function Invoke-ListIntuneReusableSettings { Entrypoint .ROLE Endpoint.MEM.Read + .DESCRIPTION + Lists Intune reusable policy settings for a tenant. Supports UseReportDB=true query parameter to retrieve cached data from the reporting database for significantly better performance, especially when querying AllTenants. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneScript.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneScript.ps1 index d2ce25ce75c2..0d2ce38965a9 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneScript.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneScript.ps1 @@ -4,6 +4,8 @@ function Invoke-ListIntuneScript { Entrypoint .ROLE Endpoint.MEM.Read + .DESCRIPTION + Lists Intune device management scripts (Windows, macOS, Linux, and remediation scripts) for a tenant. Supports UseReportDB=true query parameter to retrieve cached data from the reporting database for significantly better performance, especially when querying AllTenants. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroups.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroups.ps1 index fd3db975e50b..1882e1f92d2c 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroups.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroups.ps1 @@ -4,6 +4,8 @@ function Invoke-ListGroups { Entrypoint .ROLE Identity.Group.Read + .DESCRIPTION + Lists Entra ID groups for a tenant, including group members and owners. Supports UseReportDB=true query parameter to retrieve cached data from the reporting database for significantly better performance, especially when querying AllTenants. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListMFAUsers.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListMFAUsers.ps1 index 9446c664eef3..71a1ecb4ef5e 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListMFAUsers.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListMFAUsers.ps1 @@ -4,6 +4,8 @@ function Invoke-ListMFAUsers { Entrypoint .ROLE Identity.User.Read + .DESCRIPTION + Lists users and their MFA registration status for a tenant. Supports UseReportDB=true query parameter to retrieve cached data from the reporting database for significantly better performance, especially when querying AllTenants. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Invoke-ListMDEOnboarding.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Invoke-ListMDEOnboarding.ps1 index 1b2f1651f02e..af05672aef92 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Invoke-ListMDEOnboarding.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Invoke-ListMDEOnboarding.ps1 @@ -4,6 +4,8 @@ function Invoke-ListMDEOnboarding { Entrypoint .ROLE Security.Defender.Read + .DESCRIPTION + Lists Microsoft Defender for Endpoint onboarding status for devices in a tenant. Supports UseReportDB=true query parameter to retrieve cached data from the reporting database for significantly better performance. Automatically uses the reporting database when querying AllTenants. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSites.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSites.ps1 index 0335d3357557..c18da7996be7 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSites.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSites.ps1 @@ -4,6 +4,8 @@ function Invoke-ListSites { Entrypoint .ROLE Sharepoint.Site.Read + .DESCRIPTION + Lists SharePoint sites or OneDrive usage for a tenant. Requires a Type parameter (SharePointSiteUsage or OneDriveUsageAccount). Supports UseReportDB=true query parameter to retrieve cached data from the reporting database for significantly better performance, especially when querying AllTenants. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListTeams.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListTeams.ps1 index 1aac6035189b..fce39a1df912 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListTeams.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListTeams.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListTeams { Entrypoint .ROLE Teams.Group.Read + .DESCRIPTION + Lists Microsoft Teams teams for a tenant. Supports UseReportDB=true query parameter to retrieve cached data from the reporting database for significantly better performance, especially when querying AllTenants. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListTeamsActivity.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListTeamsActivity.ps1 index 7130578223f9..88ae24d43ad7 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListTeamsActivity.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListTeamsActivity.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListTeamsActivity { Entrypoint .ROLE Teams.Activity.Read + .DESCRIPTION + Lists Microsoft Teams user activity reports for a tenant. Supports UseReportDB=true query parameter to retrieve cached data from the reporting database for significantly better performance, especially when querying AllTenants. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListTeamsVoice.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListTeamsVoice.ps1 index 33b34c6b37a8..a52f51d00d54 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListTeamsVoice.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListTeamsVoice.ps1 @@ -4,6 +4,8 @@ function Invoke-ListTeamsVoice { Entrypoint .ROLE Teams.Voice.Read + .DESCRIPTION + Lists Microsoft Teams voice and PSTN usage for a tenant. Supports UseReportDB=true query parameter to retrieve cached data from the reporting database for significantly better performance, especially when querying AllTenants. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Reports/Invoke-ListOAuthApps.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Reports/Invoke-ListOAuthApps.ps1 index 54a00103ec5a..9b31c25d0df1 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Reports/Invoke-ListOAuthApps.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Reports/Invoke-ListOAuthApps.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListOAuthApps { Entrypoint .ROLE Tenant.Application.Read + .DESCRIPTION + Lists OAuth application consent grants and permissions for a tenant. Supports UseReportDB=true query parameter to retrieve cached data from the reporting database for significantly better performance, especially when querying AllTenants. #> [CmdletBinding()] param($Request, $TriggerMetadata) From c7cebfeb4ff1cb4565926cf653b62c92a96cd505 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 4 Jun 2026 13:50:07 +0200 Subject: [PATCH 132/202] better descriptions --- .../Invoke-ListCalendarPermissions.ps1 | 2 +- .../Administration/Invoke-ListHVEAccounts.ps1 | 26 +++++++++---------- .../Tenant/Reports/Invoke-ListOAuthApps.ps1 | 2 +- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListCalendarPermissions.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListCalendarPermissions.ps1 index 93171cef4ef7..b57cc92b29b9 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListCalendarPermissions.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListCalendarPermissions.ps1 @@ -1,4 +1,4 @@ -Function Invoke-ListCalendarPermissions { +function Invoke-ListCalendarPermissions { <# .FUNCTIONALITY Entrypoint diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListHVEAccounts.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListHVEAccounts.ps1 index 98cc5d49ff4b..a77495b98d15 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListHVEAccounts.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListHVEAccounts.ps1 @@ -19,8 +19,8 @@ function Invoke-ListHVEAccounts { if ($ListBillingPolicies -eq 'true') { # Return available HVE billing policies for the tenant $GraphRequest = @(New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-BillingPolicy' -cmdParams @{ - ResourceType = 'HVE' - }) + ResourceType = 'HVE' + }) return ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::OK @@ -106,17 +106,17 @@ function Invoke-ListHVEAccounts { # Live EXO query $GraphRequest = (New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-MailUser' -cmdParams @{ - HVEAccount = $true - } -Select 'DisplayName,PrimarySmtpAddress,ExternalDirectoryObjectId,Alias,WhenCreated,EmailAddresses,HiddenFromAddressListsEnabled') | Select-Object ` - @{ Name = 'displayName'; Expression = { $_.DisplayName } }, - @{ Name = 'primarySmtpAddress'; Expression = { $_.PrimarySmtpAddress } }, - @{ Name = 'recipientType'; Expression = { 'MailUser' } }, - @{ Name = 'recipientTypeDetails'; Expression = { 'HVEAccount' } }, - ExternalDirectoryObjectId, - Alias, - WhenCreated, - @{ Name = 'AdditionalEmailAddresses'; Expression = { ($_.'EmailAddresses' | Where-Object { $_ -clike 'smtp:*' }).Replace('smtp:', '') -join ', ' } }, - HiddenFromAddressListsEnabled + HVEAccount = $true + } -Select 'DisplayName,PrimarySmtpAddress,ExternalDirectoryObjectId,Alias,WhenCreated,EmailAddresses,HiddenFromAddressListsEnabled') | Select-Object ` + @{ Name = 'displayName'; Expression = { $_.DisplayName } }, + @{ Name = 'primarySmtpAddress'; Expression = { $_.PrimarySmtpAddress } }, + @{ Name = 'recipientType'; Expression = { 'MailUser' } }, + @{ Name = 'recipientTypeDetails'; Expression = { 'HVEAccount' } }, + ExternalDirectoryObjectId, + Alias, + WhenCreated, + @{ Name = 'AdditionalEmailAddresses'; Expression = { ($_.'EmailAddresses' | Where-Object { $_ -clike 'smtp:*' }).Replace('smtp:', '') -join ', ' } }, + HiddenFromAddressListsEnabled $StatusCode = [HttpStatusCode]::OK } catch { diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Reports/Invoke-ListOAuthApps.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Reports/Invoke-ListOAuthApps.ps1 index 9b31c25d0df1..863c2948d407 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Reports/Invoke-ListOAuthApps.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Reports/Invoke-ListOAuthApps.ps1 @@ -1,4 +1,4 @@ -Function Invoke-ListOAuthApps { +function Invoke-ListOAuthApps { <# .FUNCTIONALITY Entrypoint From 6a1c373363007e65c6cd2cac5ee6bd568d608c3e Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 4 Jun 2026 15:08:35 +0200 Subject: [PATCH 133/202] fixes issue with all tenants retrieval --- .../Public/Entrypoints/HTTP Functions/Invoke-ListDBCache.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListDBCache.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListDBCache.ps1 index 2377cd41f106..9262aac23a32 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListDBCache.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListDBCache.ps1 @@ -14,7 +14,7 @@ function Invoke-ListDBCache { $TenantFilter = $Request.Query.tenantFilter $Type = $Request.Query.type - $Tenant = (Get-Tenants -TenantFilter $TenantFilter).defaultDomainName + $Tenant = if ($TenantFilter -ne 'AllTenants') { (Get-Tenants -TenantFilter $TenantFilter).defaultDomainName } else { $TenantFilter } if (-not $TenantFilter) { return ([HttpResponseContext]@{ From 4909e9c2094e9ad497d2c5decfe3f3d1070f6a26 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 4 Jun 2026 15:09:37 +0200 Subject: [PATCH 134/202] dbcache desc --- .../HTTP Functions/Invoke-ListDBCache.ps1 | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListDBCache.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListDBCache.ps1 index 9262aac23a32..8f7e5b15ac8e 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListDBCache.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListDBCache.ps1 @@ -4,6 +4,32 @@ function Invoke-ListDBCache { Entrypoint .ROLE CIPP.Core.Read + .DESCRIPTION + Retrieves cached tenant data from the CIPP reporting database (CippReportingDB). This is the fastest + and most efficient way to query tenant data across single or multiple tenants. The database is populated + nightly by background cache jobs, so data is typically at most 24 hours old. + + Required query parameters: + - tenantFilter: The tenant domain or 'AllTenants' to query all managed tenants. + - type: The cache collection to retrieve (e.g. Users, Groups, Mailboxes, Devices, etc.). + + Use type=_availableTypes to discover which cache collections exist for a given tenant. Omitting the + type parameter also returns the available types. + + PERFORMANCE GUIDANCE: For AllTenants queries or any bulk/cross-tenant data retrieval, prefer + ListDBCache over calling individual endpoints (e.g. ListUsers, ListGroups, ListMailboxes) directly. + Individual endpoints make live API calls per tenant which is significantly slower and may hit + throttling limits. ListDBCache reads pre-cached data from Azure Table Storage and returns results + in seconds regardless of tenant count. + + Recommended workflow for MCP tool selection: + 1. Call ListDBCache with type=_availableTypes to discover available cache collections. + 2. If the data you need exists as a cache type, use ListDBCache with that type. + 3. Only fall back to individual List* endpoints when you need real-time data for a single tenant + or when the data is not available in the cache. + + Common cache types include: Users, Groups, Mailboxes, Devices, ConditionalAccess, Applications, + IntunePolicy, CompliancePolicy, and many more. The exact set depends on what has been configured. #> [CmdletBinding()] param ( From 3ca58875ac346e65937655ced8cf044603df75ca Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 4 Jun 2026 15:17:52 +0200 Subject: [PATCH 135/202] updated descriptions. --- .../CIPP/Core/Invoke-ListAdminPortalLicenses.ps1 | 2 ++ .../HTTP Functions/CIPP/Core/Invoke-ListApiTest.ps1 | 2 ++ .../CIPP/Core/Invoke-ListCustomDataMappings.ps1 | 2 ++ .../CIPP/Core/Invoke-ListDiagnosticsPresets.ps1 | 2 ++ .../HTTP Functions/CIPP/Core/Invoke-ListDirectoryObjects.ps1 | 2 ++ .../HTTP Functions/CIPP/Core/Invoke-ListFeatureFlags.ps1 | 2 ++ .../HTTP Functions/CIPP/Core/Invoke-ListGraphBulkRequest.ps1 | 2 ++ .../HTTP Functions/CIPP/Core/Invoke-ListGraphRequest.ps1 | 2 ++ .../HTTP Functions/CIPP/Core/Invoke-ListSnoozedAlerts.ps1 | 2 ++ .../HTTP Functions/CIPP/Core/invoke-ListEmptyResults.ps1 | 2 ++ .../CIPP/Extensions/Invoke-ListExtensionSync.ps1 | 2 ++ .../CIPP/Scheduler/Invoke-ListScheduledItemDetails.ps1 | 2 ++ .../CIPP/Scheduler/Invoke-ListScheduledItems.ps1 | 2 ++ .../HTTP Functions/CIPP/Settings/Invoke-ListCIPPUsers.ps1 | 2 ++ .../HTTP Functions/CIPP/Settings/Invoke-ListContainerLogs.ps1 | 2 ++ .../HTTP Functions/CIPP/Settings/Invoke-ListCustomRole.ps1 | 2 ++ .../CIPP/Settings/Invoke-ListCustomVariables.ps1 | 2 ++ .../CIPP/Settings/Invoke-ListExcludedLicenses.ps1 | 2 ++ .../HTTP Functions/CIPP/Settings/Invoke-ListTenantGroups.ps1 | 2 ++ .../HTTP Functions/CIPP/Settings/Invoke-ListWorkerHealth.ps1 | 2 ++ .../Administration/Contacts/Invoke-ListContactTemplates.ps1 | 2 ++ .../Administration/Contacts/Invoke-ListContacts.ps1 | 2 ++ .../Administration/Invoke-ListContactPermissions.ps1 | 2 ++ .../Administration/Invoke-ListMailboxMobileDevices.ps1 | 2 ++ .../Email-Exchange/Administration/Invoke-ListOoO.ps1 | 2 ++ .../Administration/Invoke-ListSharedMailboxStatistics.ps1 | 2 ++ .../Email-Exchange/Reports/Invoke-ListActiveSyncDevices.ps1 | 2 ++ .../Email-Exchange/Reports/Invoke-ListAntiPhishingFilters.ps1 | 2 ++ .../Email-Exchange/Reports/Invoke-ListGlobalAddressList.ps1 | 2 ++ .../Email-Exchange/Reports/Invoke-ListMailboxCAS.ps1 | 2 ++ .../Email-Exchange/Reports/Invoke-ListMalwareFilters.ps1 | 2 ++ .../Reports/Invoke-ListSafeAttachmentsFilters.ps1 | 2 ++ .../Reports/Invoke-ListSharedMailboxAccountEnabled.ps1 | 2 ++ .../Email-Exchange/Resources/Invoke-ListEquipment.ps1 | 2 ++ .../Email-Exchange/Resources/Invoke-ListRoomLists.ps1 | 2 ++ .../Email-Exchange/Resources/Invoke-ListRooms.ps1 | 2 ++ .../Email-Exchange/Spamfilter/Invoke-ListConnectionFilter.ps1 | 2 ++ .../Spamfilter/Invoke-ListConnectionFilterTemplates.ps1 | 2 ++ .../Email-Exchange/Spamfilter/Invoke-ListMailQuarantine.ps1 | 2 ++ .../Spamfilter/Invoke-ListMailQuarantineMessage.ps1 | 2 ++ .../Email-Exchange/Spamfilter/Invoke-ListQuarantinePolicy.ps1 | 2 ++ .../Spamfilter/Invoke-ListSpamFilterTemplates.ps1 | 2 ++ .../Email-Exchange/Spamfilter/Invoke-ListSpamfilter.ps1 | 2 ++ .../Spamfilter/Invoke-ListTenantAllowBlockListTemplates.ps1 | 2 ++ .../Email-Exchange/Tools/Invoke-ListExoRequest.ps1 | 2 ++ .../Email-Exchange/Tools/Invoke-ListMailboxRestores.ps1 | 2 ++ .../Email-Exchange/Tools/Invoke-ListMessageTrace.ps1 | 2 ++ .../Transport/Invoke-ListExConnectorTemplates.ps1 | 2 ++ .../Transport/Invoke-ListExchangeConnectors.ps1 | 2 ++ .../Email-Exchange/Transport/Invoke-ListTransportRules.ps1 | 2 ++ .../Transport/Invoke-ListTransportRulesTemplates.ps1 | 2 ++ .../Endpoint/Applications/Invoke-ListApplicationQueue.ps1 | 2 ++ .../Endpoint/Applications/Invoke-ListAppsRepository.ps1 | 2 ++ .../Endpoint/Autopilot/Invoke-ListAPDevices.ps1 | 2 ++ .../Endpoint/Autopilot/Invoke-ListAutopilotconfig.ps1 | 2 ++ .../HTTP Functions/Endpoint/MEM/Invoke-ListDefenderState.ps1 | 2 ++ .../HTTP Functions/Endpoint/MEM/Invoke-ListDefenderTVM.ps1 | 2 ++ .../HTTP Functions/Endpoint/Reports/Invoke-ListDevices.ps1 | 2 ++ .../Groups/Invoke-ListGroupSenderAuthentication.ps1 | 2 ++ .../Identity/Administration/Users/Invoke-ListDeletedItems.ps1 | 2 ++ .../Administration/Users/Invoke-ListJITAdminTemplates.ps1 | 2 ++ .../Administration/Users/Invoke-ListNewUserDefaults.ps1 | 2 ++ .../Identity/Administration/Users/Invoke-ListPerUserMFA.ps1 | 2 ++ .../Users/Invoke-ListUserConditionalAccessPolicies.ps1 | 2 ++ .../Identity/Administration/Users/Invoke-ListUserCounts.ps1 | 2 ++ .../Identity/Administration/Users/Invoke-ListUserDevices.ps1 | 2 ++ .../Identity/Administration/Users/Invoke-ListUserGroups.ps1 | 2 ++ .../Administration/Users/Invoke-ListUserMailboxDetails.ps1 | 2 ++ .../Administration/Users/Invoke-ListUserMailboxRules.ps1 | 2 ++ .../Identity/Administration/Users/Invoke-ListUserPhoto.ps1 | 2 ++ .../Identity/Administration/Users/Invoke-ListUserSettings.ps1 | 2 ++ .../Administration/Users/Invoke-ListUserSigninLogs.ps1 | 2 ++ .../Users/Invoke-ListUserTrustedBlockedSenders.ps1 | 4 +++- .../Identity/Administration/Users/Invoke-ListUsers.ps1 | 2 ++ .../Identity/Reports/Invoke-ListAzureADConnectStatus.ps1 | 2 ++ .../HTTP Functions/Identity/Reports/Invoke-ListBasicAuth.ps1 | 2 ++ .../Identity/Reports/Invoke-ListInactiveAccounts.ps1 | 2 ++ .../HTTP Functions/Identity/Reports/Invoke-ListSignIns.ps1 | 2 ++ .../HTTP Functions/Invoke-ListAllTenantDeviceCompliance.ps1 | 2 ++ .../Entrypoints/HTTP Functions/Invoke-ListAppStatus.ps1 | 2 ++ .../Entrypoints/HTTP Functions/Invoke-ListBreachesAccount.ps1 | 2 ++ .../Entrypoints/HTTP Functions/Invoke-ListBreachesTenant.ps1 | 2 ++ .../Entrypoints/HTTP Functions/Invoke-ListCSPLicenses.ps1 | 2 ++ .../Public/Entrypoints/HTTP Functions/Invoke-ListCSPsku.ps1 | 2 ++ .../Entrypoints/HTTP Functions/Invoke-ListCheckExtAlerts.ps1 | 2 ++ .../Entrypoints/HTTP Functions/Invoke-ListCippQueue.ps1 | 2 ++ .../HTTP Functions/Invoke-ListDetectedAppDevices.ps1 | 2 ++ .../Entrypoints/HTTP Functions/Invoke-ListDetectedApps.ps1 | 2 ++ .../Entrypoints/HTTP Functions/Invoke-ListDeviceDetails.ps1 | 2 ++ .../HTTP Functions/Invoke-ListExtensionsConfig.ps1 | 2 ++ .../HTTP Functions/Invoke-ListExternalTenantInfo.ps1 | 2 ++ .../Entrypoints/HTTP Functions/Invoke-ListFunctionStats.ps1 | 2 ++ .../HTTP Functions/Invoke-ListGenericTestFunction.ps1 | 2 ++ .../HTTP Functions/Invoke-ListGraphExplorerPresets.ps1 | 2 ++ .../Entrypoints/HTTP Functions/Invoke-ListHaloClients.ps1 | 2 ++ .../Entrypoints/HTTP Functions/Invoke-ListIPWhitelist.ps1 | 2 ++ .../Entrypoints/HTTP Functions/Invoke-ListIntuneIntents.ps1 | 2 ++ .../Entrypoints/HTTP Functions/Invoke-ListKnownIPDb.ps1 | 2 ++ .../Public/Entrypoints/HTTP Functions/Invoke-ListLicenses.ps1 | 2 ++ .../Public/Entrypoints/HTTP Functions/Invoke-ListLogs.ps1 | 2 ++ .../Entrypoints/HTTP Functions/Invoke-ListNamedLocations.ps1 | 2 ++ .../HTTP Functions/Invoke-ListNotificationConfig.ps1 | 2 ++ .../Public/Entrypoints/HTTP Functions/Invoke-ListOrg.ps1 | 2 ++ .../HTTP Functions/Invoke-ListPartnerRelationships.ps1 | 2 ++ .../Entrypoints/HTTP Functions/Invoke-ListPendingWebhooks.ps1 | 2 ++ .../Entrypoints/HTTP Functions/Invoke-ListPotentialApps.ps1 | 2 ++ .../HTTP Functions/Invoke-ListResellerRelationshipLink.ps1 | 2 ++ .../HTTP Functions/Invoke-ListTenantAllowBlockList.ps1 | 2 ++ .../Entrypoints/HTTP Functions/Invoke-ListUsersAndGroups.ps1 | 2 ++ .../Compliance-DLP/Invoke-ListDlpCompliancePolicy.ps1 | 2 ++ .../Invoke-ListDlpCompliancePolicyTemplates.ps1 | 2 ++ .../Invoke-ListRetentionCompliancePolicy.ps1 | 2 ++ .../Invoke-ListRetentionCompliancePolicyTemplates.ps1 | 2 ++ .../Security/Compliance-SIT/Invoke-ListSensitiveInfoType.ps1 | 2 ++ .../Compliance-SIT/Invoke-ListSensitiveInfoTypeTemplates.ps1 | 2 ++ .../Invoke-ListSensitivityLabel.ps1 | 2 ++ .../Invoke-ListSensitivityLabelTemplates.ps1 | 2 ++ .../Safe-Links-Policy/Invoke-ListSafeLinksPolicyTemplates.ps1 | 2 ++ .../Teams-Sharepoint/Invoke-ListSharepointAdminUrl.ps1 | 2 ++ .../Teams-Sharepoint/Invoke-ListSharepointQuota.ps1 | 2 ++ .../Teams-Sharepoint/Invoke-ListSharepointSettings.ps1 | 2 ++ .../Teams-Sharepoint/Invoke-ListSiteMembers.ps1 | 2 ++ .../Teams-Sharepoint/Invoke-ListTeamsLisLocation.ps1 | 2 ++ .../Tenant/Administration/Alerts/Invoke-ListAlertsQueue.ps1 | 2 ++ .../Administration/Alerts/Invoke-ListAuditLogSearches.ps1 | 2 ++ .../Tenant/Administration/Alerts/Invoke-ListAuditLogTest.ps1 | 2 ++ .../Tenant/Administration/Alerts/Invoke-ListAuditLogs.ps1 | 2 ++ .../Tenant/Administration/Alerts/Invoke-ListWebhookAlert.ps1 | 2 ++ .../Application Approval/Invoke-ListAppApprovalTemplates.ps1 | 2 ++ .../Tenant/Administration/Invoke-ListAppConsentRequests.ps1 | 2 ++ .../Tenant/Administration/Invoke-ListDomains.ps1 | 2 ++ .../Tenant/Administration/Invoke-ListOffboardTenants.ps1 | 2 ++ .../Tenant/Administration/Invoke-ListTenantOnboarding.ps1 | 2 ++ .../Tenant/Administration/Tenant/Invoke-ListTenantDetails.ps1 | 2 ++ .../Tenant/Administration/Tenant/Invoke-ListTenants.ps1 | 2 ++ .../Tenant/Conditional/Invoke-ListCAtemplates.ps1 | 2 ++ .../Conditional/Invoke-ListConditionalAccessPolicies.ps1 | 2 ++ .../Conditional/Invoke-ListConditionalAccessPolicyChanges.ps1 | 2 ++ .../Tenant/GDAP/Invoke-ListGDAPAccessAssignments.ps1 | 2 ++ .../HTTP Functions/Tenant/GDAP/Invoke-ListGDAPContracts.ps1 | 2 ++ .../HTTP Functions/Tenant/GDAP/Invoke-ListGDAPInvite.ps1 | 2 ++ .../Tenant/GDAP/Invoke-ListGDAPRelationships.ps1 | 2 ++ .../HTTP Functions/Tenant/GDAP/Invoke-ListGDAPRoles.ps1 | 2 ++ .../Tenant/GDAP/Invoke-ListGDAPServicePrincipals.ps1 | 2 ++ .../HTTP Functions/Tenant/Reports/Invoke-ListGraphReports.ps1 | 2 ++ .../Tenant/Reports/Invoke-ListLicensesReport.ps1 | 2 ++ .../Tenant/Reports/Invoke-ListServiceHealth.ps1 | 2 ++ .../HTTP Functions/Tenant/Standards/Invoke-ListBPA.ps1 | 2 ++ .../Tenant/Standards/Invoke-ListBPATemplates.ps1 | 2 ++ .../Tenant/Standards/Invoke-ListDomainAnalyser.ps1 | 2 ++ .../Tenant/Standards/Invoke-ListDomainHealth.ps1 | 2 ++ .../HTTP Functions/Tenant/Standards/Invoke-ListStandards.ps1 | 2 ++ .../Tenant/Standards/Invoke-ListStandardsCompare.ps1 | 2 ++ .../Tenant/Standards/Invoke-ListStandardsCurrentState.ps1 | 2 ++ .../Tenant/Standards/Invoke-ListTenantAlignment.ps1 | 2 ++ .../Tenant/Standards/Invoke-ListTenantDrift.ps1 | 2 ++ .../Tenant/Standards/Invoke-listStandardTemplates.ps1 | 2 ++ .../Tools/Custom-Scripts/Invoke-ListCustomScripts.ps1 | 2 ++ .../HTTP Functions/Tools/Invoke-ListGeneratedReports.ps1 | 2 ++ .../Tools/Invoke-ListReportBuilderTemplates.ps1 | 2 ++ 160 files changed, 321 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListAdminPortalLicenses.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListAdminPortalLicenses.ps1 index dbbd60370647..ac979e6d7427 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListAdminPortalLicenses.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListAdminPortalLicenses.ps1 @@ -4,6 +4,8 @@ function Invoke-ListAdminPortalLicenses { Entrypoint .ROLE CIPP.Core.Read + .DESCRIPTION + Retrieves license information from the Microsoft 365 Admin Portal for a tenant, including low-friction trial allotments. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListApiTest.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListApiTest.ps1 index 56784611a481..ac2d3ca6f91d 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListApiTest.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListApiTest.ps1 @@ -4,6 +4,8 @@ function Invoke-ListApiTest { Entrypoint,AnyTenant .ROLE CIPP.Core.Read + .DESCRIPTION + Returns debug information about the current API request, trigger metadata, and environment variables. Used for CIPP platform diagnostics. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListCustomDataMappings.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListCustomDataMappings.ps1 index 87f6a88e7429..057072b1bb55 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListCustomDataMappings.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListCustomDataMappings.ps1 @@ -4,6 +4,8 @@ function Invoke-ListCustomDataMappings { Entrypoint .ROLE CIPP.Core.Read + .DESCRIPTION + Lists custom data mappings that define how external data sources map to CIPP directory objects, filterable by source type, directory object, and tenant. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListDiagnosticsPresets.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListDiagnosticsPresets.ps1 index a52b7c6e0982..d1847b05a664 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListDiagnosticsPresets.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListDiagnosticsPresets.ps1 @@ -4,6 +4,8 @@ function Invoke-ListDiagnosticsPresets { Entrypoint .ROLE CIPP.SuperAdmin.Read + .DESCRIPTION + Lists saved diagnostics presets used for troubleshooting CIPP configuration. Requires SuperAdmin access. #> [CmdletBinding()] param ( diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListDirectoryObjects.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListDirectoryObjects.ps1 index 9d9201b17e1c..360f777b7d06 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListDirectoryObjects.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListDirectoryObjects.ps1 @@ -4,6 +4,8 @@ function Invoke-ListDirectoryObjects { Entrypoint,AnyTenant .ROLE CIPP.Core.Read + .DESCRIPTION + Resolves Entra ID directory objects by their IDs using the directoryObjects/getByIds batch endpoint. Accepts an array of object IDs in the request body. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListFeatureFlags.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListFeatureFlags.ps1 index 70304808efa0..a5a2b1b74bd3 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListFeatureFlags.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListFeatureFlags.ps1 @@ -4,6 +4,8 @@ function Invoke-ListFeatureFlags { Entrypoint .ROLE CIPP.Core.Read + .DESCRIPTION + Lists CIPP feature flags and their enabled/disabled state, including environment-driven overrides. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListGraphBulkRequest.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListGraphBulkRequest.ps1 index deef9107d572..ce3772aabe64 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListGraphBulkRequest.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListGraphBulkRequest.ps1 @@ -4,6 +4,8 @@ function Invoke-ListGraphBulkRequest { Entrypoint .ROLE CIPP.Core.Read + .DESCRIPTION + Executes multiple Microsoft Graph API requests in a single batch call for a given tenant. Accepts an array of request objects in the body. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListGraphRequest.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListGraphRequest.ps1 index aed9bf3143d8..1e34cc74a409 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListGraphRequest.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListGraphRequest.ps1 @@ -5,6 +5,8 @@ function Invoke-ListGraphRequest { Entrypoint .ROLE CIPP.Core.Read + .DESCRIPTION + Proxies an arbitrary Microsoft Graph API GET request for a tenant. Supports custom endpoints, filters, pagination, and field selection via query parameters. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListSnoozedAlerts.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListSnoozedAlerts.ps1 index bb3503c9636a..b4e897b7999a 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListSnoozedAlerts.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListSnoozedAlerts.ps1 @@ -4,6 +4,8 @@ function Invoke-ListSnoozedAlerts { Entrypoint,AnyTenant .ROLE CIPP.Alert.Read + .DESCRIPTION + Lists alerts that have been snoozed (temporarily suppressed), filterable by cmdlet name. Returns snooze duration and scope details. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/invoke-ListEmptyResults.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/invoke-ListEmptyResults.ps1 index 066dda75515a..a1a627eb15cb 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/invoke-ListEmptyResults.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/invoke-ListEmptyResults.ps1 @@ -8,6 +8,8 @@ function invoke-ListEmptyResults { Entrypoint,AnyTenant .ROLE CIPP.Core.Read + .DESCRIPTION + Returns an empty results array. Used as a placeholder endpoint. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Extensions/Invoke-ListExtensionSync.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Extensions/Invoke-ListExtensionSync.ps1 index f9b6ae869289..90674124ee4b 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Extensions/Invoke-ListExtensionSync.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Extensions/Invoke-ListExtensionSync.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListExtensionSync { Entrypoint,AnyTenant .ROLE CIPP.Extension.Read + .DESCRIPTION + Lists extension synchronization tasks and their status, including last sync results for configured third-party integrations (Hudu, Halo, NinjaOne, etc.). #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-ListScheduledItemDetails.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-ListScheduledItemDetails.ps1 index c3608506733f..ddbe7f88ea36 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-ListScheduledItemDetails.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-ListScheduledItemDetails.ps1 @@ -4,6 +4,8 @@ function Invoke-ListScheduledItemDetails { Entrypoint,AnyTenant .ROLE CIPP.Scheduler.Read + .DESCRIPTION + Retrieves detailed information about a specific scheduled task by its RowKey, including execution results and task parameters. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-ListScheduledItems.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-ListScheduledItems.ps1 index 0bfc9afc6019..32ad490284a0 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-ListScheduledItems.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-ListScheduledItems.ps1 @@ -4,6 +4,8 @@ function Invoke-ListScheduledItems { Entrypoint,AnyTenant .ROLE CIPP.Scheduler.Read + .DESCRIPTION + Lists scheduled tasks in CIPP, filterable by tenant or task ID. Returns task name, command, schedule, and last execution status. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListCIPPUsers.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListCIPPUsers.ps1 index 0e60dcb07c25..6d9afe1fc368 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListCIPPUsers.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListCIPPUsers.ps1 @@ -4,6 +4,8 @@ function Invoke-ListCIPPUsers { Entrypoint,AnyTenant .ROLE CIPP.SuperAdmin.Read + .DESCRIPTION + Lists CIPP platform users and their role assignments from the allowedUsers table. Requires SuperAdmin access. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListContainerLogs.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListContainerLogs.ps1 index a98c001c05b8..f31799a046da 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListContainerLogs.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListContainerLogs.ps1 @@ -4,6 +4,8 @@ function Invoke-ListContainerLogs { Entrypoint,AnyTenant .ROLE CIPP.SuperAdmin.Read + .DESCRIPTION + Retrieves container logs for the CIPP backend process. Requires SuperAdmin access. Supports ReadLog, ListFiles, and snapshot actions. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListCustomRole.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListCustomRole.ps1 index 08e84177482a..7479dedda3be 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListCustomRole.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListCustomRole.ps1 @@ -4,6 +4,8 @@ function Invoke-ListCustomRole { Entrypoint .ROLE CIPP.Core.Read + .DESCRIPTION + Lists custom RBAC roles defined in CIPP, including their permission sets and associated access role groups. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListCustomVariables.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListCustomVariables.ps1 index 46c5649d9b96..48bd343bbfdd 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListCustomVariables.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListCustomVariables.ps1 @@ -4,6 +4,8 @@ function Invoke-ListCustomVariables { Entrypoint .ROLE CIPP.Core.Read + .DESCRIPTION + Lists custom variables configured in CIPP for use in templates and automation. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListExcludedLicenses.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListExcludedLicenses.ps1 index 06ddff582e2a..00494bb4c2c0 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListExcludedLicenses.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListExcludedLicenses.ps1 @@ -4,6 +4,8 @@ function Invoke-ListExcludedLicenses { Entrypoint .ROLE CIPP.AppSettings.Read + .DESCRIPTION + Lists license SKUs that have been excluded from CIPP license counts and reporting. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListTenantGroups.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListTenantGroups.ps1 index d546f23ba30c..3fef931ce04c 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListTenantGroups.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListTenantGroups.ps1 @@ -6,6 +6,8 @@ function Invoke-ListTenantGroups { Entrypoint,AnyTenant .ROLE CIPP.Core.Read + .DESCRIPTION + Lists tenant groups (logical groupings of managed tenants) and optionally includes usage data. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListWorkerHealth.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListWorkerHealth.ps1 index 80177f19e9c6..fe5ffefa19fd 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListWorkerHealth.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListWorkerHealth.ps1 @@ -4,6 +4,8 @@ function Invoke-ListWorkerHealth { Entrypoint,AnyTenant .ROLE CIPP.SuperAdmin.Read + .DESCRIPTION + Retrieves health status and diagnostics for CIPP background worker processes. Requires SuperAdmin access. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-ListContactTemplates.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-ListContactTemplates.ps1 index 6e7757a17548..b42fa55a7562 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-ListContactTemplates.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-ListContactTemplates.ps1 @@ -4,6 +4,8 @@ function Invoke-ListContactTemplates { Entrypoint,AnyTenant .ROLE Exchange.Contact.Read + .DESCRIPTION + Lists saved mail contact templates used for creating new Exchange contacts. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-ListContacts.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-ListContacts.ps1 index 1a8b64a3c3a9..82eedf839f8d 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-ListContacts.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-ListContacts.ps1 @@ -7,6 +7,8 @@ function Invoke-ListContacts { Entrypoint .ROLE Exchange.Contact.Read + .DESCRIPTION + Lists Exchange Online mail contacts for a tenant, with optional filtering by contact ID for detailed view. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListContactPermissions.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListContactPermissions.ps1 index 1b06863c1fb7..ea70f90b7125 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListContactPermissions.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListContactPermissions.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListContactPermissions { Entrypoint .ROLE Exchange.Mailbox.Read + .DESCRIPTION + Lists folder-level permissions on a user's Contacts folder in Exchange Online. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxMobileDevices.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxMobileDevices.ps1 index daf056a0297c..979f53cc9ee0 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxMobileDevices.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxMobileDevices.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListMailboxMobileDevices { Entrypoint .ROLE Exchange.Mailbox.Read + .DESCRIPTION + Lists mobile devices associated with a specific Exchange Online mailbox. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListOoO.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListOoO.ps1 index e014458f2c9f..d27dcab2bcb6 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListOoO.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListOoO.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListOoO { Entrypoint .ROLE Exchange.Mailbox.Read + .DESCRIPTION + Retrieves Out of Office (automatic reply) settings for a specific Exchange Online user. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListSharedMailboxStatistics.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListSharedMailboxStatistics.ps1 index 75c3eeddfec5..f345f13786c5 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListSharedMailboxStatistics.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListSharedMailboxStatistics.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListSharedMailboxStatistics { Entrypoint .ROLE Exchange.Mailbox.Read + .DESCRIPTION + Lists shared mailboxes and their usage statistics for a tenant. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListActiveSyncDevices.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListActiveSyncDevices.ps1 index c07406ab1794..6de5d25d6e72 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListActiveSyncDevices.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListActiveSyncDevices.ps1 @@ -4,6 +4,8 @@ function Invoke-ListActiveSyncDevices { Entrypoint .ROLE Exchange.Mailbox.Read + .DESCRIPTION + Lists all ActiveSync mobile devices registered across Exchange Online mailboxes in a tenant. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListAntiPhishingFilters.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListAntiPhishingFilters.ps1 index e5f77dbe1893..52c0dfc4070a 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListAntiPhishingFilters.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListAntiPhishingFilters.ps1 @@ -4,6 +4,8 @@ function Invoke-ListAntiPhishingFilters { Entrypoint .ROLE Exchange.SpamFilter.Read + .DESCRIPTION + Lists anti-phishing policies and their associated rules configured in Exchange Online Protection. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListGlobalAddressList.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListGlobalAddressList.ps1 index c54aca7cd798..efdfda67fa5c 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListGlobalAddressList.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListGlobalAddressList.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListGlobalAddressList { Entrypoint .ROLE Exchange.Mailbox.Read + .DESCRIPTION + Lists all recipients in the Exchange Online Global Address List, including display names, email addresses, and recipient types. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListMailboxCAS.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListMailboxCAS.ps1 index b8a43c2667fa..4ec43894b7c7 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListMailboxCAS.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListMailboxCAS.ps1 @@ -4,6 +4,8 @@ function Invoke-ListMailboxCAS { Entrypoint .ROLE Exchange.Mailbox.Read + .DESCRIPTION + Lists Client Access Settings (CAS) for Exchange Online mailboxes, showing which protocols are enabled (OWA, IMAP, POP, MAPI, EWS, ActiveSync). #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListMalwareFilters.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListMalwareFilters.ps1 index 05a1152b65b2..3a73e9cf0efd 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListMalwareFilters.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListMalwareFilters.ps1 @@ -4,6 +4,8 @@ function Invoke-ListMalwareFilters { Entrypoint .ROLE Exchange.SpamFilter.Read + .DESCRIPTION + Lists malware filter policies and their associated rules configured in Exchange Online Protection. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListSafeAttachmentsFilters.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListSafeAttachmentsFilters.ps1 index 1e0d39b2ea83..84fbf6742484 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListSafeAttachmentsFilters.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListSafeAttachmentsFilters.ps1 @@ -4,6 +4,8 @@ function Invoke-ListSafeAttachmentsFilters { Entrypoint .ROLE Exchange.SpamFilter.Read + .DESCRIPTION + Lists Safe Attachments policies and their associated rules configured in Microsoft Defender for Office 365. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListSharedMailboxAccountEnabled.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListSharedMailboxAccountEnabled.ps1 index a0e307c1cba8..6daf75698761 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListSharedMailboxAccountEnabled.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListSharedMailboxAccountEnabled.ps1 @@ -4,6 +4,8 @@ function Invoke-ListSharedMailboxAccountEnabled { Entrypoint .ROLE Exchange.Mailbox.Read + .DESCRIPTION + Lists shared mailboxes that have direct sign-in enabled (account not disabled), which is a security concern. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-ListEquipment.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-ListEquipment.ps1 index df15735141a3..5e2278a5fc1f 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-ListEquipment.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-ListEquipment.ps1 @@ -4,6 +4,8 @@ function Invoke-ListEquipment { Entrypoint .ROLE Exchange.Equipment.Read + .DESCRIPTION + Lists equipment mailboxes (projectors, vehicles, etc.) in Exchange Online for a tenant. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-ListRoomLists.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-ListRoomLists.ps1 index a21b783ea50b..b9af8cf9d5a4 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-ListRoomLists.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-ListRoomLists.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListRoomLists { Entrypoint .ROLE Exchange.Room.Read + .DESCRIPTION + Lists room list distribution groups and their members in Exchange Online. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-ListRooms.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-ListRooms.ps1 index 39079ac4a331..127b5d4006fc 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-ListRooms.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-ListRooms.ps1 @@ -4,6 +4,8 @@ function Invoke-ListRooms { Entrypoint .ROLE Exchange.Room.Read + .DESCRIPTION + Lists room mailboxes (meeting rooms) in Exchange Online for a tenant, with optional detailed view by room ID. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListConnectionFilter.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListConnectionFilter.ps1 index 20b531f56a56..cd9c98b3fcce 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListConnectionFilter.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListConnectionFilter.ps1 @@ -4,6 +4,8 @@ function Invoke-ListConnectionFilter { Entrypoint .ROLE Exchange.ConnectionFilter.Read + .DESCRIPTION + Lists hosted connection filter policies (IP allow/block lists) in Exchange Online Protection. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListConnectionFilterTemplates.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListConnectionFilterTemplates.ps1 index 6ae0ccc0e18e..3846b7c9a9a4 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListConnectionFilterTemplates.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListConnectionFilterTemplates.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListConnectionFilterTemplates { Entrypoint,AnyTenant .ROLE Exchange.ConnectionFilter.Read + .DESCRIPTION + Lists saved connection filter policy templates for Exchange Online Protection. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListMailQuarantine.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListMailQuarantine.ps1 index 7e61444ea96c..f0ec05dce631 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListMailQuarantine.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListMailQuarantine.ps1 @@ -4,6 +4,8 @@ function Invoke-ListMailQuarantine { Entrypoint .ROLE Exchange.SpamFilter.Read + .DESCRIPTION + Lists quarantined email messages in Exchange Online Protection for a tenant. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListMailQuarantineMessage.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListMailQuarantineMessage.ps1 index d83c59360755..22a89fd7ff43 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListMailQuarantineMessage.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListMailQuarantineMessage.ps1 @@ -4,6 +4,8 @@ function Invoke-ListMailQuarantineMessage { Entrypoint .ROLE Exchange.SpamFilter.Read + .DESCRIPTION + Exports and retrieves the raw EML content of a specific quarantined email message by its Identity. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListQuarantinePolicy.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListQuarantinePolicy.ps1 index 79ef6d2c172f..12018e4046b7 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListQuarantinePolicy.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListQuarantinePolicy.ps1 @@ -4,6 +4,8 @@ function Invoke-ListQuarantinePolicy { Entrypoint .ROLE Exchange.SpamFilter.Read + .DESCRIPTION + Lists quarantine policies configured in Exchange Online Protection, controlling end-user access to quarantined messages. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListSpamFilterTemplates.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListSpamFilterTemplates.ps1 index 2c2ec2be3037..f64a2f9e2e4c 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListSpamFilterTemplates.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListSpamFilterTemplates.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListSpamFilterTemplates { Entrypoint,AnyTenant .ROLE Exchange.SpamFilter.Read + .DESCRIPTION + Lists saved spam filter policy templates for Exchange Online Protection. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListSpamfilter.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListSpamfilter.ps1 index 34ac01d062bc..85332ac2aaa2 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListSpamfilter.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListSpamfilter.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListSpamfilter { Entrypoint .ROLE Exchange.SpamFilter.Read + .DESCRIPTION + Lists hosted content filter (anti-spam) policies and their rule states in Exchange Online Protection. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListTenantAllowBlockListTemplates.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListTenantAllowBlockListTemplates.ps1 index bf1c33fb86c3..b620f882923c 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListTenantAllowBlockListTemplates.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListTenantAllowBlockListTemplates.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListTenantAllowBlockListTemplates { Entrypoint,AnyTenant .ROLE Exchange.SpamFilter.Read + .DESCRIPTION + Lists saved Tenant Allow/Block List templates for Exchange Online Protection. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Tools/Invoke-ListExoRequest.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Tools/Invoke-ListExoRequest.ps1 index 852efdf0fde2..b2a160cd135b 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Tools/Invoke-ListExoRequest.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Tools/Invoke-ListExoRequest.ps1 @@ -4,6 +4,8 @@ function Invoke-ListExoRequest { Entrypoint .ROLE CIPP.Core.Read + .DESCRIPTION + Executes an arbitrary read-only Exchange Online cmdlet (Get-* or Search-*) for a tenant. Accepts cmdlet name and parameters in the request body. #> param($Request, $TriggerMetadata) try { diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Tools/Invoke-ListMailboxRestores.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Tools/Invoke-ListMailboxRestores.ps1 index bb1dff0b0cfb..0c4c705c8492 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Tools/Invoke-ListMailboxRestores.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Tools/Invoke-ListMailboxRestores.ps1 @@ -4,6 +4,8 @@ function Invoke-ListMailboxRestores { Entrypoint .ROLE Exchange.Mailbox.Read + .DESCRIPTION + Lists mailbox restore requests and their status for Exchange Online, with optional statistics for a specific restore operation. #> param($Request, $TriggerMetadata) # Interact with query parameters or the body of the request. diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Tools/Invoke-ListMessageTrace.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Tools/Invoke-ListMessageTrace.ps1 index 2822893a97ac..c6f2cd495d67 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Tools/Invoke-ListMessageTrace.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Tools/Invoke-ListMessageTrace.ps1 @@ -4,6 +4,8 @@ function Invoke-ListMessageTrace { Entrypoint .ROLE Exchange.TransportRule.Read + .DESCRIPTION + Traces email message delivery in Exchange Online, searchable by message ID, sender, recipient, and date range. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Transport/Invoke-ListExConnectorTemplates.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Transport/Invoke-ListExConnectorTemplates.ps1 index 46567c22b80f..c94ad02a045a 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Transport/Invoke-ListExConnectorTemplates.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Transport/Invoke-ListExConnectorTemplates.ps1 @@ -4,6 +4,8 @@ function Invoke-ListExConnectorTemplates { Entrypoint,AnyTenant .ROLE Exchange.Connector.Read + .DESCRIPTION + Lists saved Exchange connector templates for creating inbound/outbound connectors. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Transport/Invoke-ListExchangeConnectors.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Transport/Invoke-ListExchangeConnectors.ps1 index a0455bfb6137..abda27d47325 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Transport/Invoke-ListExchangeConnectors.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Transport/Invoke-ListExchangeConnectors.ps1 @@ -4,6 +4,8 @@ function Invoke-ListExchangeConnectors { Entrypoint .ROLE Exchange.Connector.Read + .DESCRIPTION + Lists inbound and outbound mail flow connectors configured in Exchange Online. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Transport/Invoke-ListTransportRules.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Transport/Invoke-ListTransportRules.ps1 index 27f5cb7af8a2..eeb62913014f 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Transport/Invoke-ListTransportRules.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Transport/Invoke-ListTransportRules.ps1 @@ -4,6 +4,8 @@ function Invoke-ListTransportRules { Entrypoint .ROLE Exchange.TransportRule.Read + .DESCRIPTION + Lists mail flow (transport) rules configured in Exchange Online, with optional detailed view by rule ID. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Transport/Invoke-ListTransportRulesTemplates.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Transport/Invoke-ListTransportRulesTemplates.ps1 index 22e0b1a406c3..b2d45b7cca89 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Transport/Invoke-ListTransportRulesTemplates.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Transport/Invoke-ListTransportRulesTemplates.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListTransportRulesTemplates { Entrypoint,AnyTenant .ROLE Exchange.TransportRule.Read + .DESCRIPTION + Lists saved transport rule templates for Exchange Online mail flow rules. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-ListApplicationQueue.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-ListApplicationQueue.ps1 index aa9b53145eb4..67b6c2cad152 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-ListApplicationQueue.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-ListApplicationQueue.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListApplicationQueue { Entrypoint .ROLE Endpoint.Application.Read + .DESCRIPTION + Lists queued Intune application deployments that are pending assignment to tenants. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-ListAppsRepository.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-ListAppsRepository.ps1 index c79b99f2550e..6c3da70cc6ed 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-ListAppsRepository.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-ListAppsRepository.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListAppsRepository { Entrypoint,AnyTenant .ROLE Endpoint.Application.Read + .DESCRIPTION + Searches external application repositories (WinGet, Chocolatey, etc.) for available application packages. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Autopilot/Invoke-ListAPDevices.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Autopilot/Invoke-ListAPDevices.ps1 index 929fc774db76..35cc7d437edd 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Autopilot/Invoke-ListAPDevices.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Autopilot/Invoke-ListAPDevices.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListAPDevices { Entrypoint .ROLE Endpoint.Autopilot.Read + .DESCRIPTION + Lists Windows Autopilot device identities registered in a tenant. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Autopilot/Invoke-ListAutopilotconfig.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Autopilot/Invoke-ListAutopilotconfig.ps1 index 458782d1215d..aad4740a7111 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Autopilot/Invoke-ListAutopilotconfig.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Autopilot/Invoke-ListAutopilotconfig.ps1 @@ -4,6 +4,8 @@ function Invoke-ListAutopilotconfig { Entrypoint .ROLE Endpoint.Autopilot.Read + .DESCRIPTION + Lists Windows Autopilot deployment profiles, Enrollment Status Page configurations, or Windows Hello for Business policies for a tenant. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListDefenderState.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListDefenderState.ps1 index 4ca3690502b5..7971e2bfdf5b 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListDefenderState.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListDefenderState.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListDefenderState { Entrypoint .ROLE Endpoint.MEM.Read + .DESCRIPTION + Lists Microsoft Defender antivirus state and detected threats for Intune-managed devices in a tenant. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListDefenderTVM.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListDefenderTVM.ps1 index ad2065be4e42..0431e666b0cb 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListDefenderTVM.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListDefenderTVM.ps1 @@ -4,6 +4,8 @@ function Invoke-ListDefenderTVM { Entrypoint .ROLE Endpoint.MEM.Read + .DESCRIPTION + Lists software vulnerabilities from Microsoft Defender Threat and Vulnerability Management (TVM), grouped by CVE ID. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Reports/Invoke-ListDevices.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Reports/Invoke-ListDevices.ps1 index de9b40413023..6775937a0a2c 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Reports/Invoke-ListDevices.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Reports/Invoke-ListDevices.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListDevices { Entrypoint .ROLE Endpoint.Device.Read + .DESCRIPTION + Lists Intune-managed devices for a tenant, including device compliance and configuration status. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroupSenderAuthentication.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroupSenderAuthentication.ps1 index 203b28046a6d..c17306da70ef 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroupSenderAuthentication.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroupSenderAuthentication.ps1 @@ -4,6 +4,8 @@ function Invoke-ListGroupSenderAuthentication { Entrypoint .ROLE Exchange.Groups.Read + .DESCRIPTION + Lists sender authentication settings for an Exchange distribution group or mail-enabled security group, controlling who can send to the group. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListDeletedItems.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListDeletedItems.ps1 index 72fed7ce5282..da218c5c91c2 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListDeletedItems.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListDeletedItems.ps1 @@ -4,6 +4,8 @@ function Invoke-ListDeletedItems { Entrypoint .ROLE Tenant.Directory.Read + .DESCRIPTION + Lists soft-deleted directory objects in Entra ID, including users, groups, applications, service principals, and administrative units. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListJITAdminTemplates.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListJITAdminTemplates.ps1 index acdcaf4ccb42..fa517ddbcf23 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListJITAdminTemplates.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListJITAdminTemplates.ps1 @@ -4,6 +4,8 @@ function Invoke-ListJITAdminTemplates { Entrypoint,AnyTenant .ROLE Identity.Role.Read + .DESCRIPTION + Lists Just-in-Time admin role templates that define temporary admin role assignments. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListNewUserDefaults.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListNewUserDefaults.ps1 index 847e7271c95f..468a2c2c2386 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListNewUserDefaults.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListNewUserDefaults.ps1 @@ -4,6 +4,8 @@ function Invoke-ListNewUserDefaults { Entrypoint,AnyTenant .ROLE Identity.User.Read + .DESCRIPTION + Lists default templates for new user creation, including default domain, usage location, and license assignments. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListPerUserMFA.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListPerUserMFA.ps1 index bc75662b6414..307d84d39454 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListPerUserMFA.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListPerUserMFA.ps1 @@ -4,6 +4,8 @@ function Invoke-ListPerUserMFA { Entrypoint .ROLE Identity.User.Read + .DESCRIPTION + Lists per-user MFA state (enabled, enforced, disabled) for users in a tenant via the legacy MFA management API. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserConditionalAccessPolicies.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserConditionalAccessPolicies.ps1 index 3ff24cbfa7a4..f13e72c76824 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserConditionalAccessPolicies.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserConditionalAccessPolicies.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListUserConditionalAccessPolicies { Entrypoint .ROLE Identity.User.Read + .DESCRIPTION + Lists Conditional Access policies that apply to a specific user in a tenant. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserCounts.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserCounts.ps1 index c77673e9774f..8e840b76a2b7 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserCounts.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserCounts.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListUserCounts { Entrypoint .ROLE Identity.User.Read + .DESCRIPTION + Returns summary counts of total users, licensed users, and global administrators for a tenant. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserDevices.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserDevices.ps1 index 3cb8910846b7..19b218c1980c 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserDevices.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserDevices.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListUserDevices { Entrypoint .ROLE Identity.User.Read + .DESCRIPTION + Lists Intune-managed devices registered to a specific user, including compliance status and endpoint protection details. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserGroups.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserGroups.ps1 index 176a7dfa61d3..da79a08e6dc0 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserGroups.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserGroups.ps1 @@ -4,6 +4,8 @@ function Invoke-ListUserGroups { Entrypoint .ROLE Identity.User.Read + .DESCRIPTION + Lists Entra ID group memberships for a specific user. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserMailboxDetails.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserMailboxDetails.ps1 index 01d95a67a2d5..97cebd8840b9 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserMailboxDetails.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserMailboxDetails.ps1 @@ -4,6 +4,8 @@ function Invoke-ListUserMailboxDetails { Entrypoint .ROLE Exchange.Mailbox.Read + .DESCRIPTION + Retrieves detailed Exchange Online mailbox properties for a specific user, including quotas, archive status, and protocols. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserMailboxRules.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserMailboxRules.ps1 index 43f031c7ec5a..e29e630c511d 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserMailboxRules.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserMailboxRules.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListUserMailboxRules { Entrypoint .ROLE Exchange.Mailbox.Read + .DESCRIPTION + Lists inbox rules configured on a specific user's Exchange Online mailbox. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserPhoto.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserPhoto.ps1 index 0c02bac81d50..8e262240fa31 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserPhoto.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserPhoto.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListUserPhoto { Entrypoint,AnyTenant .ROLE Identity.User.Read + .DESCRIPTION + Retrieves the profile photo for a specific Entra ID user, returned as a base64-encoded image. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserSettings.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserSettings.ps1 index 322b089e3de3..e539f7765414 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserSettings.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserSettings.ps1 @@ -4,6 +4,8 @@ function Invoke-ListUserSettings { Entrypoint,AnyTenant .ROLE Identity.User.Read + .DESCRIPTION + Retrieves the current CIPP user's personal settings and preferences. #> param($Request, $TriggerMetadata) $Headers = $Request.Headers diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserSigninLogs.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserSigninLogs.ps1 index 298fb893da2f..53cf9dd2f0e5 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserSigninLogs.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserSigninLogs.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListUserSigninLogs { Entrypoint .ROLE Identity.User.Read + .DESCRIPTION + Lists recent sign-in log entries for a specific Entra ID user, ordered by most recent. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserTrustedBlockedSenders.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserTrustedBlockedSenders.ps1 index 96e89c7397f4..ddb90c36df87 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserTrustedBlockedSenders.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserTrustedBlockedSenders.ps1 @@ -1,9 +1,11 @@ -function Invoke-ListUserTrustedBlockedSenders { +function Invoke-ListUserTrustedBlockedSenders { <# .FUNCTIONALITY Entrypoint .ROLE Exchange.Mailbox.Read + .DESCRIPTION + Lists trusted and blocked sender entries configured for a specific user's Exchange Online mailbox junk email settings. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUsers.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUsers.ps1 index 70eccf33048c..843fc4159857 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUsers.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUsers.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListUsers { Entrypoint .ROLE Identity.User.Read + .DESCRIPTION + Lists Entra ID users for a tenant with license and sign-in details, or retrieves a specific user by ID. For AllTenants or cached data, consider using ListDBCache with type=Users for significantly better performance. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListAzureADConnectStatus.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListAzureADConnectStatus.ps1 index 02275366ff54..9dda69a4c3be 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListAzureADConnectStatus.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListAzureADConnectStatus.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListAzureADConnectStatus { Entrypoint .ROLE Tenant.Directory.Read + .DESCRIPTION + Retrieves Entra ID Connect (Azure AD Connect) synchronization status and configuration for a tenant, including sync intervals, password sync, and pass-through authentication settings. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListBasicAuth.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListBasicAuth.ps1 index 06b90ed29bb4..7a4a90f64238 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListBasicAuth.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListBasicAuth.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListBasicAuth { Entrypoint .ROLE Identity.AuditLog.Read + .DESCRIPTION + Lists sign-in events using basic authentication (legacy protocols) for a tenant from the audit logs. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListInactiveAccounts.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListInactiveAccounts.ps1 index 382218496777..a562ed80527e 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListInactiveAccounts.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListInactiveAccounts.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListInactiveAccounts { Entrypoint .ROLE Tenant.Directory.Read + .DESCRIPTION + Lists user accounts that have not signed in for a configurable number of days (default 180), based on sign-in activity data. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListSignIns.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListSignIns.ps1 index 9139264ad86d..6bb5e6ceec6f 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListSignIns.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListSignIns.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListSignIns { Entrypoint .ROLE Identity.AuditLog.Read + .DESCRIPTION + Lists recent sign-in log entries for a tenant, filterable by various criteria. Supports AllTenants queries. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListAllTenantDeviceCompliance.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListAllTenantDeviceCompliance.ps1 index 9a6a1ec56e99..9035f74899b2 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListAllTenantDeviceCompliance.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListAllTenantDeviceCompliance.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListAllTenantDeviceCompliance { Entrypoint .ROLE Tenant.DeviceCompliance.Read + .DESCRIPTION + Lists device compliance summary across all managed tenants using the Lighthouse managedDeviceCompliances API, or for a single tenant via Intune. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListAppStatus.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListAppStatus.ps1 index 1a71e1e78851..be44eaaa3746 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListAppStatus.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListAppStatus.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListAppStatus { Entrypoint .ROLE Endpoint.Device.Read + .DESCRIPTION + Lists Intune application installation status across devices for a specific application, including install state and error codes. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListBreachesAccount.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListBreachesAccount.ps1 index d4deeb37f6f0..f61fb6c9a997 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListBreachesAccount.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListBreachesAccount.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListBreachesAccount { Entrypoint .ROLE CIPP.Core.Read + .DESCRIPTION + Checks an email account or domain against the Have I Been Pwned (HIBP) breach database. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListBreachesTenant.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListBreachesTenant.ps1 index 76fcb3767415..ed8619691093 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListBreachesTenant.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListBreachesTenant.ps1 @@ -4,6 +4,8 @@ function Invoke-ListBreachesTenant { Entrypoint .ROLE CIPP.Core.Read + .DESCRIPTION + Lists cached data breach results for user accounts in a tenant from the CIPP breach monitoring cache. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListCSPLicenses.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListCSPLicenses.ps1 index dea7d823c1c1..6408e039be3b 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListCSPLicenses.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListCSPLicenses.ps1 @@ -4,6 +4,8 @@ function Invoke-ListCSPLicenses { Entrypoint .ROLE Tenant.Directory.Read + .DESCRIPTION + Lists CSP (Cloud Solution Provider) license subscriptions for a tenant via the Sherweb integration. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListCSPsku.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListCSPsku.ps1 index b317057f1870..f6fad33a4cad 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListCSPsku.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListCSPsku.ps1 @@ -4,6 +4,8 @@ function Invoke-ListCSPsku { Entrypoint .ROLE Tenant.Directory.Read + .DESCRIPTION + Lists available CSP SKUs and current subscriptions for a tenant via the Sherweb integration. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListCheckExtAlerts.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListCheckExtAlerts.ps1 index 95533bce6e7c..66b2e563fcc3 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListCheckExtAlerts.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListCheckExtAlerts.ps1 @@ -4,6 +4,8 @@ function Invoke-ListCheckExtAlerts { Entrypoint .ROLE CIPP.Core.Read + .DESCRIPTION + Lists extension alert check results from third-party integrations for a tenant. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListCippQueue.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListCippQueue.ps1 index c76d786be3d1..14f41c20c7c6 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListCippQueue.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListCippQueue.ps1 @@ -4,6 +4,8 @@ function Invoke-ListCippQueue { Entrypoint,AnyTenant .ROLE CIPP.Core.Read + .DESCRIPTION + Lists active and recent CIPP background processing queue items and their status. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListDetectedAppDevices.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListDetectedAppDevices.ps1 index e394db309281..0798fb24b952 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListDetectedAppDevices.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListDetectedAppDevices.ps1 @@ -4,6 +4,8 @@ function Invoke-ListDetectedAppDevices { Entrypoint .ROLE Identity.Device.Read + .DESCRIPTION + Lists Intune-managed devices that have a specific detected application installed, identified by App ID. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListDetectedApps.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListDetectedApps.ps1 index 0a8a754c271a..62a275bf454d 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListDetectedApps.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListDetectedApps.ps1 @@ -4,6 +4,8 @@ function Invoke-ListDetectedApps { Entrypoint .ROLE Identity.Device.Read + .DESCRIPTION + Lists applications detected on Intune-managed devices in a tenant, optionally including device associations. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListDeviceDetails.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListDeviceDetails.ps1 index cc7a2a3d7c01..e28da15109bc 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListDeviceDetails.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListDeviceDetails.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListDeviceDetails { Entrypoint .ROLE Identity.Device.Read + .DESCRIPTION + Retrieves detailed information about a specific Intune-managed device by ID, name, or serial number. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListExtensionsConfig.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListExtensionsConfig.ps1 index 494b04b7b782..33fbf202cf3b 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListExtensionsConfig.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListExtensionsConfig.ps1 @@ -4,6 +4,8 @@ function Invoke-ListExtensionsConfig { Entrypoint,AnyTenant .ROLE CIPP.Extension.Read + .DESCRIPTION + Lists the current CIPP extension configuration, including settings for HaloPSA, NinjaOne, Hudu, and other integrations. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListExternalTenantInfo.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListExternalTenantInfo.ps1 index e1fe3dc11a16..cd142ee4173c 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListExternalTenantInfo.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListExternalTenantInfo.ps1 @@ -4,6 +4,8 @@ function Invoke-ListExternalTenantInfo { Entrypoint,AnyTenant .ROLE CIPP.Core.Read + .DESCRIPTION + Looks up publicly available tenant information for an external Entra ID tenant by domain or tenant ID. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListFunctionStats.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListFunctionStats.ps1 index 0035fa7e0ef5..6e571db949e0 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListFunctionStats.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListFunctionStats.ps1 @@ -4,6 +4,8 @@ function Invoke-ListFunctionStats { Entrypoint .ROLE CIPP.Core.Read + .DESCRIPTION + Lists execution statistics for CIPP functions, including timing and performance metrics, filterable by function type and time range. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListGenericTestFunction.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListGenericTestFunction.ps1 index e3c94cde337f..eec7cad107c2 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListGenericTestFunction.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListGenericTestFunction.ps1 @@ -4,6 +4,8 @@ function Invoke-ListGenericTestFunction { Entrypoint .ROLE CIPP.Core.Read + .DESCRIPTION + Returns the original request URL for debugging purposes. Used for CIPP platform diagnostics. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListGraphExplorerPresets.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListGraphExplorerPresets.ps1 index 1900839d6c15..6cb0d6a833f1 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListGraphExplorerPresets.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListGraphExplorerPresets.ps1 @@ -4,6 +4,8 @@ function Invoke-ListGraphExplorerPresets { Entrypoint,AnyTenant .ROLE CIPP.Core.Read + .DESCRIPTION + Lists saved Graph Explorer query presets for the current user or shared presets. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListHaloClients.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListHaloClients.ps1 index 12cab4b9461d..d67bdd99ea86 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListHaloClients.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListHaloClients.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListHaloClients { Entrypoint .ROLE CIPP.Extension.Read + .DESCRIPTION + Lists client records from the configured HaloPSA instance for tenant mapping purposes. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListIPWhitelist.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListIPWhitelist.ps1 index 68731c9707d7..7d58e5c2bc48 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListIPWhitelist.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListIPWhitelist.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListIPWhitelist { Entrypoint .ROLE CIPP.Core.Read + .DESCRIPTION + Lists trusted IP addresses configured in CIPP for IP-based access control. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListIntuneIntents.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListIntuneIntents.ps1 index 227ebcdf09b3..b74b1b14a1d7 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListIntuneIntents.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListIntuneIntents.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListIntuneIntents { Entrypoint .ROLE Endpoint.MEM.Read + .DESCRIPTION + Lists Intune security baseline and endpoint protection intents (legacy template-based policies) with their settings and categories. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListKnownIPDb.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListKnownIPDb.ps1 index 5c7f7102130b..acb6deb0c703 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListKnownIPDb.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListKnownIPDb.ps1 @@ -4,6 +4,8 @@ function Invoke-ListKnownIPDb { Entrypoint .ROLE CIPP.Core.Read + .DESCRIPTION + Lists known IP address entries from the CIPP IP database, optionally filtered by tenant. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListLicenses.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListLicenses.ps1 index c241e26c0736..9be3623fb381 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListLicenses.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListLicenses.ps1 @@ -4,6 +4,8 @@ function Invoke-ListLicenses { Entrypoint .ROLE Tenant.Directory.Read + .DESCRIPTION + Lists Microsoft 365 license SKUs and their assigned/available counts for a tenant. For AllTenants queries, consider using ListDBCache for better performance. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListLogs.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListLogs.ps1 index 2cff4d234fbe..57326d72c52c 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListLogs.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListLogs.ps1 @@ -4,6 +4,8 @@ function Invoke-ListLogs { Entrypoint,AnyTenant .ROLE CIPP.Core.Read + .DESCRIPTION + Lists CIPP platform audit logs with filtering by severity, date range, tenant, and user. Supports listing available log categories. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListNamedLocations.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListNamedLocations.ps1 index 2af340c043c8..a03958737448 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListNamedLocations.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListNamedLocations.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListNamedLocations { Entrypoint .ROLE Tenant.ConditionalAccess.Read + .DESCRIPTION + Lists Conditional Access named locations (IP ranges and country-based locations) configured in a tenant. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListNotificationConfig.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListNotificationConfig.ps1 index a1312b27ad66..a25d520f38d9 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListNotificationConfig.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListNotificationConfig.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListNotificationConfig { Entrypoint .ROLE CIPP.AppSettings.Read + .DESCRIPTION + Retrieves the CIPP notification configuration, including alert channels, severity filters, and notification targets. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListOrg.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListOrg.ps1 index 894f43e5ff4f..428af197807e 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListOrg.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListOrg.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListOrg { Entrypoint .ROLE CIPP.Core.Read + .DESCRIPTION + Retrieves the Entra ID organization profile for a tenant, including display name, addresses, and tenant metadata. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListPartnerRelationships.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListPartnerRelationships.ps1 index 848fc7ab498d..5c080a61a535 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListPartnerRelationships.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListPartnerRelationships.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListPartnerRelationships { Entrypoint .ROLE Tenant.Relationship.Read + .DESCRIPTION + Lists cross-tenant access policy partner configurations for a tenant, showing delegated admin and collaboration settings. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListPendingWebhooks.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListPendingWebhooks.ps1 index 3e27835e30d0..a1cd999c3bd7 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListPendingWebhooks.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListPendingWebhooks.ps1 @@ -4,6 +4,8 @@ function Invoke-ListPendingWebhooks { Entrypoint .ROLE CIPP.Alert.Read + .DESCRIPTION + Lists pending incoming webhook events that have not yet been processed by CIPP. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListPotentialApps.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListPotentialApps.ps1 index 305b2e645c4c..1657905efabe 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListPotentialApps.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListPotentialApps.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListPotentialApps { Entrypoint .ROLE Endpoint.Application.Read + .DESCRIPTION + Searches application repositories (WinGet, Chocolatey) for available applications matching a search string. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListResellerRelationshipLink.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListResellerRelationshipLink.ps1 index 1d48e1a8591e..f3a92f9571a3 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListResellerRelationshipLink.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListResellerRelationshipLink.ps1 @@ -4,6 +4,8 @@ function Invoke-ListResellerRelationshipLink { Entrypoint,AnyTenant .ROLE Tenant.Relationship.Read + .DESCRIPTION + Retrieves the indirect reseller relationship invitation link from Partner Center for onboarding new customer tenants. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListTenantAllowBlockList.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListTenantAllowBlockList.ps1 index a69e6f7ca5cb..70a91d8159bb 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListTenantAllowBlockList.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListTenantAllowBlockList.ps1 @@ -4,6 +4,8 @@ function Invoke-ListTenantAllowBlockList { Entrypoint .ROLE Exchange.SpamFilter.Read + .DESCRIPTION + Lists Tenant Allow/Block List entries (senders, URLs, file hashes, IPs) from Exchange Online Protection. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListUsersAndGroups.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListUsersAndGroups.ps1 index 4064ec8345bd..af6261d5fac5 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListUsersAndGroups.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListUsersAndGroups.ps1 @@ -4,6 +4,8 @@ function Invoke-ListUsersAndGroups { Entrypoint .ROLE Tenant.Directory.Read + .DESCRIPTION + Lists both users and groups for a tenant in a single batch call, returning ID and display name for selection/lookup purposes. #> [CmdletBinding()] param($Request, $TriggerMetadata) 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 index 26fcd8eafb9e..a223c8a383f5 100644 --- 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 @@ -4,6 +4,8 @@ Function Invoke-ListDlpCompliancePolicy { Entrypoint .ROLE Security.DlpCompliancePolicy.Read + .DESCRIPTION + Lists Data Loss Prevention (DLP) compliance policies and their associated rules from the Security & Compliance Center. #> [CmdletBinding()] param($Request, $TriggerMetadata) 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 index a958e9acd2ee..9664c6855b4f 100644 --- 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 @@ -4,6 +4,8 @@ Function Invoke-ListDlpCompliancePolicyTemplates { Entrypoint,AnyTenant .ROLE Security.DlpCompliancePolicy.Read + .DESCRIPTION + Lists saved DLP compliance policy templates for deploying standardized DLP configurations. #> [CmdletBinding()] param($Request, $TriggerMetadata) 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 index 1c5290932c8f..c8aa16e24275 100644 --- 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 @@ -4,6 +4,8 @@ Function Invoke-ListRetentionCompliancePolicy { Entrypoint .ROLE Security.RetentionCompliancePolicy.Read + .DESCRIPTION + Lists retention compliance policies and their associated rules from the Security & Compliance Center. #> [CmdletBinding()] param($Request, $TriggerMetadata) 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 index 98dace219fbc..7a4ba6c64213 100644 --- 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 @@ -4,6 +4,8 @@ Function Invoke-ListRetentionCompliancePolicyTemplates { Entrypoint,AnyTenant .ROLE Security.RetentionCompliancePolicy.Read + .DESCRIPTION + Lists saved retention compliance policy templates for deploying standardized retention configurations. #> [CmdletBinding()] param($Request, $TriggerMetadata) 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 index 5ae4a340fe49..fc9b982e8e5a 100644 --- 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 @@ -4,6 +4,8 @@ Function Invoke-ListSensitiveInfoType { Entrypoint .ROLE Security.SensitiveInfoType.Read + .DESCRIPTION + Lists sensitive information types (SITs) configured in the Security & Compliance Center, optionally including built-in types. #> [CmdletBinding()] param($Request, $TriggerMetadata) 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 index fa21108ce653..c51786a04bd8 100644 --- 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 @@ -4,6 +4,8 @@ Function Invoke-ListSensitiveInfoTypeTemplates { Entrypoint,AnyTenant .ROLE Security.SensitiveInfoType.Read + .DESCRIPTION + Lists saved sensitive information type templates for deploying custom SIT configurations. #> [CmdletBinding()] param($Request, $TriggerMetadata) 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 index 4c6947962223..dc24e819d687 100644 --- 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 @@ -4,6 +4,8 @@ Function Invoke-ListSensitivityLabel { Entrypoint .ROLE Security.SensitivityLabel.Read + .DESCRIPTION + Lists sensitivity labels and label policies configured in the Security & Compliance Center for a tenant. #> [CmdletBinding()] param($Request, $TriggerMetadata) 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 index ab066a4eb6d5..b3ddc62b247f 100644 --- 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 @@ -4,6 +4,8 @@ Function Invoke-ListSensitivityLabelTemplates { Entrypoint,AnyTenant .ROLE Security.SensitivityLabel.Read + .DESCRIPTION + Lists saved sensitivity label templates for deploying standardized label configurations. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicyTemplates.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicyTemplates.ps1 index 9d93753933e1..80fa2088ac39 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicyTemplates.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicyTemplates.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListSafeLinksPolicyTemplates { Entrypoint,AnyTenant .ROLE Exchange.SafeLinks.Read + .DESCRIPTION + Lists saved Safe Links policy templates for deploying standardized Safe Links configurations. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointAdminUrl.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointAdminUrl.ps1 index baa4e97d3fd0..355facd78999 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointAdminUrl.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointAdminUrl.ps1 @@ -4,6 +4,8 @@ function Invoke-ListSharepointAdminUrl { Entrypoint .ROLE CIPP.Core.Read + .DESCRIPTION + Retrieves the SharePoint Admin Center URL for a tenant. #> [CmdletBinding()] param( diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointQuota.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointQuota.ps1 index 1f2a94f7ed26..5410025042b9 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointQuota.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointQuota.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListSharepointQuota { Entrypoint .ROLE Sharepoint.Admin.Read + .DESCRIPTION + Retrieves SharePoint Online storage quota usage for a tenant, showing used and total storage. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointSettings.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointSettings.ps1 index 2b7d22e5849f..e7db82d04897 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointSettings.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointSettings.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListSharepointSettings { Entrypoint .ROLE Sharepoint.Admin.Read + .DESCRIPTION + Retrieves SharePoint Online tenant-level settings and configuration. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSiteMembers.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSiteMembers.ps1 index 4b7aff18a806..d57571cee6b3 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSiteMembers.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSiteMembers.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListSiteMembers { Entrypoint .ROLE Sharepoint.Site.Read + .DESCRIPTION + Lists members of a specific SharePoint site by site ID, including user display names and email addresses. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListTeamsLisLocation.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListTeamsLisLocation.ps1 index 8e58755752c5..a348bf6a9ac8 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListTeamsLisLocation.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListTeamsLisLocation.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListTeamsLisLocation { Entrypoint .ROLE Teams.Voice.Read + .DESCRIPTION + Lists Teams emergency calling Location Information Service (LIS) locations configured for a tenant. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListAlertsQueue.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListAlertsQueue.ps1 index b1c4aa287f2c..21ecc2dd632e 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListAlertsQueue.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListAlertsQueue.ps1 @@ -4,6 +4,8 @@ function Invoke-ListAlertsQueue { Entrypoint .ROLE CIPP.Alert.Read + .DESCRIPTION + Lists configured alert rules including webhook rules and scheduled alert tasks, showing their configuration and tenant scope. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListAuditLogSearches.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListAuditLogSearches.ps1 index ef5a2ff069d6..00943e189a11 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListAuditLogSearches.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListAuditLogSearches.ps1 @@ -4,6 +4,8 @@ function Invoke-ListAuditLogSearches { Entrypoint .ROLE Tenant.Alert.Read + .DESCRIPTION + Lists or creates Microsoft 365 Unified Audit Log searches for a tenant, with support for retrieving search results. #> Param($Request, $TriggerMetadata) # Interact with the query parameters diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListAuditLogTest.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListAuditLogTest.ps1 index 0f0efa9cbeff..7a779a245dfb 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListAuditLogTest.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListAuditLogTest.ps1 @@ -5,6 +5,8 @@ function Invoke-ListAuditLogTest { .ROLE Tenant.Alert.Read + .DESCRIPTION + Tests audit log webhook rules against a specific search to validate rule matching and alert triggering. #> Param($Request, $TriggerMetadata) $AuditLogQuery = @{ diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListAuditLogs.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListAuditLogs.ps1 index e1e7b70b57cd..b0e8b3bebef2 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListAuditLogs.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListAuditLogs.ps1 @@ -4,6 +4,8 @@ function Invoke-ListAuditLogs { Entrypoint .ROLE CIPP.Alert.Read + .DESCRIPTION + Lists audit log entries from the CIPP audit log store, filterable by tenant, date range, and log ID. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListWebhookAlert.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListWebhookAlert.ps1 index 4e4039f63657..4768e50424a1 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListWebhookAlert.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListWebhookAlert.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListWebhookAlert { Entrypoint .ROLE CIPP.Alert.Read + .DESCRIPTION + Lists configured webhook alert rules that trigger on specific audit log events. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ListAppApprovalTemplates.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ListAppApprovalTemplates.ps1 index c39ad692a9b0..ea812b960b64 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ListAppApprovalTemplates.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ListAppApprovalTemplates.ps1 @@ -4,6 +4,8 @@ function Invoke-ListAppApprovalTemplates { Entrypoint,AnyTenant .ROLE Tenant.Application.Read + .DESCRIPTION + Lists saved application approval templates for standardized app consent configurations. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ListAppConsentRequests.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ListAppConsentRequests.ps1 index 45faac54ba3a..061ac00f9ea1 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ListAppConsentRequests.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ListAppConsentRequests.ps1 @@ -4,6 +4,8 @@ function Invoke-ListAppConsentRequests { Entrypoint .ROLE Tenant.Administration.Read + .DESCRIPTION + Lists pending application consent requests in a tenant, filterable by request status. #> param($Request, $TriggerMetadata) # Interact with query parameters or the body of the request. diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ListDomains.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ListDomains.ps1 index f9662cbe6514..a72a62587c80 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ListDomains.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ListDomains.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListDomains { Entrypoint .ROLE Tenant.Administration.Read + .DESCRIPTION + Lists verified domains for a tenant, showing default and initial domain status. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ListOffboardTenants.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ListOffboardTenants.ps1 index 8768339ac2e4..dadb47c0652c 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ListOffboardTenants.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ListOffboardTenants.ps1 @@ -4,6 +4,8 @@ function Invoke-ListOffboardTenants { Entrypoint .ROLE Tenant.Administration.ReadWrite + .DESCRIPTION + Lists tenants available for offboarding, including tenants in error state. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ListTenantOnboarding.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ListTenantOnboarding.ps1 index 3fd36731e3f4..168afecc2db5 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ListTenantOnboarding.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ListTenantOnboarding.ps1 @@ -4,6 +4,8 @@ function Invoke-ListTenantOnboarding { Entrypoint .ROLE Tenant.Administration.Read + .DESCRIPTION + Lists tenant onboarding requests and their step-by-step progress, including GDAP relationship setup. #> Param($Request, $TriggerMetadata) try { diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenantDetails.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenantDetails.ps1 index 37c6ca5eb34f..f70ad515aa65 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenantDetails.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenantDetails.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListTenantDetails { Entrypoint .ROLE CIPP.Core.Read + .DESCRIPTION + Retrieves detailed organization information for a tenant, including addresses, assigned plans, sync status, and custom properties. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenants.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenants.ps1 index b319de61be43..bc9e541f0dd0 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenants.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenants.ps1 @@ -4,6 +4,8 @@ function Invoke-ListTenants { Entrypoint,AnyTenant .ROLE CIPP.Core.Read + .DESCRIPTION + Lists all managed tenants accessible to the current user, with support for cache clearing and tenant filtering. This is the primary endpoint for tenant enumeration. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ListCAtemplates.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ListCAtemplates.ps1 index d2920dbfa9b5..769a3e7c43b8 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ListCAtemplates.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ListCAtemplates.ps1 @@ -4,6 +4,8 @@ function Invoke-ListCAtemplates { Entrypoint,AnyTenant .ROLE Tenant.ConditionalAccess.Read + .DESCRIPTION + Lists saved Conditional Access policy templates for deploying standardized CA configurations. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ListConditionalAccessPolicies.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ListConditionalAccessPolicies.ps1 index f3bb2c63a33f..70a51d8e26e3 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ListConditionalAccessPolicies.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ListConditionalAccessPolicies.ps1 @@ -4,6 +4,8 @@ function Invoke-ListConditionalAccessPolicies { Entrypoint .ROLE Tenant.ConditionalAccess.Read + .DESCRIPTION + Lists Conditional Access policies for a tenant with resolved display names for users, groups, applications, and locations. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ListConditionalAccessPolicyChanges.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ListConditionalAccessPolicyChanges.ps1 index 7f8a6634063f..6cfcb589d9a2 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ListConditionalAccessPolicyChanges.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ListConditionalAccessPolicyChanges.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListConditionalAccessPolicyChanges { Entrypoint .ROLE Tenant.ConditionalAccess.Read + .DESCRIPTION + Lists audit log entries showing changes to a specific Conditional Access policy, including before/after values. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPAccessAssignments.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPAccessAssignments.ps1 index 053e11c3cdb8..e10e6e54c053 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPAccessAssignments.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPAccessAssignments.ps1 @@ -4,6 +4,8 @@ function Invoke-ListGDAPAccessAssignments { Entrypoint,AnyTenant .ROLE Tenant.Relationship.Read + .DESCRIPTION + Lists GDAP access assignments for a specific delegated admin relationship, including security group members and their role mappings. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPContracts.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPContracts.ps1 index a5d762ea88c4..21eaf43537a5 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPContracts.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPContracts.ps1 @@ -4,6 +4,8 @@ function Invoke-ListGDAPContracts { Entrypoint,AnyTenant .ROLE Tenant.Relationship.Read + .DESCRIPTION + Lists Microsoft partner contracts (customer tenant relationships) from the Graph API. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPInvite.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPInvite.ps1 index e9dd7530895d..662c6d68bcb7 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPInvite.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPInvite.ps1 @@ -4,6 +4,8 @@ function Invoke-ListGDAPInvite { Entrypoint,AnyTenant .ROLE Tenant.Relationship.Read + .DESCRIPTION + Lists GDAP relationship invitations and their role mappings, optionally filtered by relationship ID. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPRelationships.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPRelationships.ps1 index c332fc11236e..3f12e62b8beb 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPRelationships.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPRelationships.ps1 @@ -4,6 +4,8 @@ function Invoke-ListGDAPRelationships { Entrypoint,AnyTenant .ROLE Tenant.Relationship.Read + .DESCRIPTION + Lists GDAP delegated admin relationships with customer tenants, with optional filtering by relationship ID or OData filter. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPRoles.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPRoles.ps1 index d63832e0f562..6adf7f039471 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPRoles.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPRoles.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListGDAPRoles { Entrypoint,AnyTenant .ROLE Tenant.Relationship.Read + .DESCRIPTION + Lists the configured GDAP role-to-security-group mappings used for delegated admin access. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPServicePrincipals.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPServicePrincipals.ps1 index 2b5cda845dcf..72492f88e6da 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPServicePrincipals.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPServicePrincipals.ps1 @@ -4,6 +4,8 @@ function Invoke-ListGDAPServicePrincipals { Entrypoint .ROLE Tenant.Relationship.Read + .DESCRIPTION + Lists service principals in a customer tenant owned by the partner or specified vendor tenant IDs, useful for identifying GDAP-related app registrations. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Reports/Invoke-ListGraphReports.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Reports/Invoke-ListGraphReports.ps1 index efde40e3eeca..cb54777b814c 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Reports/Invoke-ListGraphReports.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Reports/Invoke-ListGraphReports.ps1 @@ -4,6 +4,8 @@ function Invoke-ListGraphReports { Entrypoint .ROLE Tenant.Reports.Read + .DESCRIPTION + Retrieves Microsoft 365 usage reports (Graph or Office reports) for a tenant, such as email activity, OneDrive usage, or Teams device usage. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Reports/Invoke-ListLicensesReport.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Reports/Invoke-ListLicensesReport.ps1 index 9a896788636a..479ebeb135af 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Reports/Invoke-ListLicensesReport.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Reports/Invoke-ListLicensesReport.ps1 @@ -4,6 +4,8 @@ function Invoke-ListLicensesReport { Entrypoint .ROLE Tenant.Directory.Read + .DESCRIPTION + Lists a detailed license overview across all tenants or a single tenant, including SKU breakdowns, costs, and availability. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Reports/Invoke-ListServiceHealth.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Reports/Invoke-ListServiceHealth.ps1 index c97d95c41d03..949257de04a8 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Reports/Invoke-ListServiceHealth.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Reports/Invoke-ListServiceHealth.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListServiceHealth { Entrypoint .ROLE Tenant.Administration.Read + .DESCRIPTION + Lists active Microsoft 365 service health issues and advisories for a tenant or all tenants. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListBPA.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListBPA.ps1 index f51ede9d33d2..97e6a98e08ba 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListBPA.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListBPA.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListBPA { Entrypoint .ROLE Tenant.BestPracticeAnalyser.Read + .DESCRIPTION + Lists Best Practice Analyser (BPA) report results for tenants, based on a selected BPA template. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListBPATemplates.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListBPATemplates.ps1 index 8680742fd0d3..dcf1ee8ffd55 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListBPATemplates.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListBPATemplates.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListBPATemplates { Entrypoint,AnyTenant .ROLE Tenant.BestPracticeAnalyser.Read + .DESCRIPTION + Lists available Best Practice Analyser (BPA) templates that define which checks to run. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListDomainAnalyser.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListDomainAnalyser.ps1 index 416db1a50c62..9fdf7227b9b9 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListDomainAnalyser.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListDomainAnalyser.ps1 @@ -5,6 +5,8 @@ Function Invoke-ListDomainAnalyser { Entrypoint .ROLE Tenant.DomainAnalyser.Read + .DESCRIPTION + Lists domain analysis results (SPF, DKIM, DMARC, DNSSEC) for tenant domains. #> [CmdletBinding()] param($Request, $TriggerMetadata) 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 2ae766dda392..35c9799b6306 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 @@ -4,6 +4,8 @@ function Invoke-ListDomainHealth { Entrypoint,AnyTenant .ROLE Tenant.DomainAnalyser.Read + .DESCRIPTION + Performs real-time DNS health checks (MX, SPF, DMARC, DKIM, DNSSEC, MTA-STS, HTTPS) for a specific domain. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListStandards.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListStandards.ps1 index 5df42f75ef80..1b9eae4ca066 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListStandards.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListStandards.ps1 @@ -4,6 +4,8 @@ Function Invoke-ListStandards { Entrypoint .ROLE Tenant.Standards.Read + .DESCRIPTION + Lists configured tenant standards (compliance policies) and their settings, with optional consolidated view. #> [CmdletBinding()] param($Request, $TriggerMetadata) 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 b3f9b53f8c4c..2741abc3e183 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 @@ -4,6 +4,8 @@ function Invoke-ListStandardsCompare { Entrypoint .ROLE Tenant.BestPracticeAnalyser.Read + .DESCRIPTION + Compares current tenant configuration against applied standards, showing compliance status and drift for each standard. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListStandardsCurrentState.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListStandardsCurrentState.ps1 index 377879c98204..a82cdff30c6f 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListStandardsCurrentState.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListStandardsCurrentState.ps1 @@ -4,6 +4,8 @@ function Invoke-ListStandardsCurrentState { Entrypoint .ROLE Tenant.Standards.Read + .DESCRIPTION + Lists the current remediation state of standards applied to a specific tenant, including last check results. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListTenantAlignment.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListTenantAlignment.ps1 index 2b79546c3e81..b123bddf07c2 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListTenantAlignment.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListTenantAlignment.ps1 @@ -4,6 +4,8 @@ function Invoke-ListTenantAlignment { Entrypoint .ROLE Tenant.Standards.Read + .DESCRIPTION + Lists tenant alignment data showing how well tenants conform to their assigned standards templates. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListTenantDrift.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListTenantDrift.ps1 index bb45a2248441..61dfaf1e9e6d 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListTenantDrift.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListTenantDrift.ps1 @@ -4,6 +4,8 @@ function Invoke-ListTenantDrift { Entrypoint .ROLE Tenant.Standards.Read + .DESCRIPTION + Lists configuration drift for tenants, comparing current state against the desired state defined by applied standards. #> [CmdletBinding()] param($Request, $TriggerMetadata) 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 de82fde161b3..1cb90266c573 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 @@ -4,6 +4,8 @@ function Invoke-listStandardTemplates { Entrypoint,AnyTenant .ROLE Tenant.Standards.Read + .DESCRIPTION + Lists saved standards templates that define sets of standards to apply to tenants. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tools/Custom-Scripts/Invoke-ListCustomScripts.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tools/Custom-Scripts/Invoke-ListCustomScripts.ps1 index 6650b82b6dd1..ef3873cf17e0 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tools/Custom-Scripts/Invoke-ListCustomScripts.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tools/Custom-Scripts/Invoke-ListCustomScripts.ps1 @@ -4,6 +4,8 @@ function Invoke-ListCustomScripts { Entrypoint .ROLE CIPP.Tests.Read + .DESCRIPTION + Lists custom PowerShell scripts stored in CIPP, with optional filtering by script GUID and version history. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tools/Invoke-ListGeneratedReports.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tools/Invoke-ListGeneratedReports.ps1 index 07799733648d..1a8811e4ea1b 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tools/Invoke-ListGeneratedReports.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tools/Invoke-ListGeneratedReports.ps1 @@ -4,6 +4,8 @@ function Invoke-ListGeneratedReports { Entrypoint .ROLE CIPP.Core.Read + .DESCRIPTION + Lists generated reports from the CIPP Report Builder, filterable by tenant or report GUID. #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tools/Invoke-ListReportBuilderTemplates.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tools/Invoke-ListReportBuilderTemplates.ps1 index 46597e031134..348a7ea79a2b 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tools/Invoke-ListReportBuilderTemplates.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tools/Invoke-ListReportBuilderTemplates.ps1 @@ -4,6 +4,8 @@ function Invoke-ListReportBuilderTemplates { Entrypoint .ROLE CIPP.Core.Read + .DESCRIPTION + Lists saved Report Builder templates that define custom report configurations with data blocks and formatting. #> [CmdletBinding()] param($Request, $TriggerMetadata) From 7902097acf76faf4ee0b6a80f2dff8cf2b0c3abd Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 4 Jun 2026 22:07:50 +0200 Subject: [PATCH 136/202] add missing return --- .../CIPP/Setup/Invoke-ExecUpdateRefreshToken.ps1 | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecUpdateRefreshToken.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecUpdateRefreshToken.ps1 index c5ed711eee83..7461952cc25c 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecUpdateRefreshToken.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecUpdateRefreshToken.ps1 @@ -31,13 +31,13 @@ function Invoke-ExecUpdateRefreshToken { Add-CIPPAzDataTableEntity @DevSecretsTable -Entity $Secret -Force } else { if ($IsPartnerTenant) { - Set-CippKeyVaultSecret -VaultName $kv -Name 'RefreshToken' -SecretValue (ConvertTo-SecureString -String $Request.body.refreshtoken -AsPlainText -Force) + $null = Set-CippKeyVaultSecret -VaultName $kv -Name 'RefreshToken' -SecretValue (ConvertTo-SecureString -String $Request.body.refreshtoken -AsPlainText -Force) Set-Item -Path env:RefreshToken -Value $Request.body.refreshtoken -Force } else { Write-Information "$($env:TenantID) does not match $($Request.body.tenantId) - adding a new secret for the tenant." $name = $Request.body.tenantId try { - Set-CippKeyVaultSecret -VaultName $kv -Name $name -SecretValue (ConvertTo-SecureString -String $Request.body.refreshtoken -AsPlainText -Force) + $null = Set-CippKeyVaultSecret -VaultName $kv -Name $name -SecretValue (ConvertTo-SecureString -String $Request.body.refreshtoken -AsPlainText -Force) Set-Item -Path env:$name -Value $Request.body.refreshtoken -Force } catch { Write-Information "Failed to set secret $name in KeyVault. $($_.Exception.Message)" @@ -60,7 +60,7 @@ function Invoke-ExecUpdateRefreshToken { OrchestratorName = 'UpdatePermissionsOrchestrator' Batch = @($TenantBatch) } - Start-CIPPOrchestrator -InputObject $InputObject + $null = Start-CIPPOrchestrator -InputObject $InputObject Write-Information 'Started permissions update orchestrator for Partner Tenant' } catch { Write-Warning "Failed to start permissions orchestrator: $($_.Exception.Message)" @@ -76,6 +76,11 @@ function Invoke-ExecUpdateRefreshToken { 'resultText' = "Successfully updated the credentials for $($TenantName). You may continue to the next step, or add additional tenants if required." 'state' = 'success' } + + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = $Results + }) } catch { $Results = [pscustomobject]@{ 'Results' = @{ From 65659c21ffad84702b6a6f62e83a39a6e59a42d9 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 4 Jun 2026 16:23:35 -0400 Subject: [PATCH 137/202] fix: remove headers parameter from scheduler details/list --- .../CIPP/Scheduler/Invoke-ListScheduledItemDetails.ps1 | 4 ++++ .../CIPP/Scheduler/Invoke-ListScheduledItems.ps1 | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-ListScheduledItemDetails.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-ListScheduledItemDetails.ps1 index ddbe7f88ea36..432943270d45 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-ListScheduledItemDetails.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-ListScheduledItemDetails.ps1 @@ -40,6 +40,10 @@ function Invoke-ListScheduledItemDetails { # Process the task (similar to the way it's done in Invoke-ListScheduledItems) if ($Task.Parameters) { $Task.Parameters = $Task.Parameters | ConvertFrom-Json -ErrorAction SilentlyContinue + # Remove headers from parameters for cleaner display, and because they may contain sensitive information. Headers are only used for execution, not needed for display. + if ($Task.Parameters.Headers) { + $Task.Parameters.PSObject.Properties.Remove('Headers') + } } else { $Task | Add-Member -NotePropertyName Parameters -NotePropertyValue @{} } diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-ListScheduledItems.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-ListScheduledItems.ps1 index 32ad490284a0..86edb6110049 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-ListScheduledItems.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-ListScheduledItems.ps1 @@ -78,6 +78,10 @@ function Invoke-ListScheduledItems { if ($Task.Parameters) { $Task.Parameters = $Task.Parameters | ConvertFrom-Json -ErrorAction SilentlyContinue + # Remove headers from parameters for cleaner display, and because they may contain sensitive information. Headers are only used for execution, not needed for display. + if ($Task.Parameters.Headers) { + $Task.Parameters.PSObject.Properties.Remove('Headers') + } } else { $Task | Add-Member -NotePropertyName Parameters -NotePropertyValue @{} } From c5d15588e31b2591514b2682d6301e9a60559c0a Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 4 Jun 2026 19:59:00 -0400 Subject: [PATCH 138/202] fix: version check --- Modules/CIPPCore/Public/Assert-CippVersion.ps1 | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Assert-CippVersion.ps1 b/Modules/CIPPCore/Public/Assert-CippVersion.ps1 index 3ee6cdd10730..8987287f29ac 100644 --- a/Modules/CIPPCore/Public/Assert-CippVersion.ps1 +++ b/Modules/CIPPCore/Public/Assert-CippVersion.ps1 @@ -11,7 +11,12 @@ function Assert-CippVersion { #> param($CIPPVersion) - $APIVersion = (Get-Content -Path (Join-Path $env:CIPPRootPath 'version_latest.txt')).trim() + + if ($env:CIPPNG -eq 'true') { + $APIVersion = $env:APP_VERSION + } else { + $APIVersion = (Get-Content -Path (Join-Path $env:CIPPRootPath 'version_latest.txt')).trim() + } $RemoteAPIVersion = (Invoke-CIPPRestMethod -Uri 'https://raw.githubusercontent.com/KelvinTegelaar/CIPP-API/master/version_latest.txt').trim() $RemoteCIPPVersion = (Invoke-CIPPRestMethod -Uri 'https://raw.githubusercontent.com/KelvinTegelaar/CIPP/main/public/version.json').version From c5849381984688f46eeb6c71e0f6ffc59acde285 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 4 Jun 2026 20:20:08 -0400 Subject: [PATCH 139/202] fix: fallback to app version if one is not specified --- Modules/CIPPCore/Public/Assert-CippVersion.ps1 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Modules/CIPPCore/Public/Assert-CippVersion.ps1 b/Modules/CIPPCore/Public/Assert-CippVersion.ps1 index 8987287f29ac..ae2485026555 100644 --- a/Modules/CIPPCore/Public/Assert-CippVersion.ps1 +++ b/Modules/CIPPCore/Public/Assert-CippVersion.ps1 @@ -14,6 +14,9 @@ function Assert-CippVersion { if ($env:CIPPNG -eq 'true') { $APIVersion = $env:APP_VERSION + if (!$CIPPVersion) { + $CIPPVersion = $env:APP_VERSION + } } else { $APIVersion = (Get-Content -Path (Join-Path $env:CIPPRootPath 'version_latest.txt')).trim() } From d4ddd3d6ff34f7a214bc58eae32eb7477b7b076e Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 4 Jun 2026 23:12:34 -0400 Subject: [PATCH 140/202] fix: additional protections for choco app deployment --- .../Public/New-CIPPIntuneAppDeployment.ps1 | 22 ++++++++++++++---- .../Applications/Invoke-AddChocoApp.ps1 | 23 +++++++++++++++++-- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/Modules/CIPPCore/Public/New-CIPPIntuneAppDeployment.ps1 b/Modules/CIPPCore/Public/New-CIPPIntuneAppDeployment.ps1 index be7ca21b9787..2b7f3dfac5ff 100644 --- a/Modules/CIPPCore/Public/New-CIPPIntuneAppDeployment.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPIntuneAppDeployment.ps1 @@ -45,21 +45,33 @@ function New-CIPPIntuneAppDeployment { } if (-not $IntuneBody -and $AppType -eq 'Choco') { + $PackageName = [string]$AppConfig.PackageName + if ([string]::IsNullOrWhiteSpace($PackageName)) { + throw 'PackageName is required for Choco app deployments.' + } + + if (-not [regex]::IsMatch($PackageName, '^[A-Za-z0-9][A-Za-z0-9._-]*$')) { + throw "Invalid PackageName '$PackageName'. Allowed characters: letters, numbers, dot, underscore, hyphen." + } + $IntuneBody = Get-Content (Join-Path $env:CIPPRootPath 'AddChocoApp\Choco.app.json') | ConvertFrom-Json $IntuneBody.description = $AppConfig.description $IntuneBody.displayName = $AppConfig.ApplicationName $IntuneBody.installExperience.runAsAccount = if ($AppConfig.InstallAsSystem) { 'system' } else { 'user' } $IntuneBody.installExperience.deviceRestartBehavior = if ($AppConfig.DisableRestart) { 'suppress' } else { 'allow' } - $IntuneBody.installCommandLine = "powershell.exe -ExecutionPolicy Bypass .\Install.ps1 -InstallChoco -Packagename $($AppConfig.PackageName)" + $PackageNameArg = ConvertTo-CIPPSafePwshArg -Value $PackageName + $IntuneBody.installCommandLine = "powershell.exe -ExecutionPolicy Bypass .\Install.ps1 -InstallChoco -Packagename $PackageNameArg" if ($AppConfig.customrepo) { - $IntuneBody.installCommandLine = $IntuneBody.installCommandLine + " -CustomRepo $($AppConfig.CustomRepo)" + $CustomRepoArg = ConvertTo-CIPPSafePwshArg -Value ([string]$AppConfig.CustomRepo) + $IntuneBody.installCommandLine = $IntuneBody.installCommandLine + " -CustomRepo $CustomRepoArg" } if ($AppConfig.customArguments) { - $IntuneBody.installCommandLine = $IntuneBody.installCommandLine + " -CustomArguments '$($AppConfig.customArguments)'" + $CustomArgumentsArg = ConvertTo-CIPPSafePwshArg -Value ([string]$AppConfig.customArguments) + $IntuneBody.installCommandLine = $IntuneBody.installCommandLine + " -CustomArguments $CustomArgumentsArg" } - $IntuneBody.UninstallCommandLine = "powershell.exe -ExecutionPolicy Bypass .\Uninstall.ps1 -Packagename $($AppConfig.PackageName)" + $IntuneBody.UninstallCommandLine = "powershell.exe -ExecutionPolicy Bypass .\Uninstall.ps1 -Packagename $PackageNameArg" $IntuneBody.detectionRules[0].path = "$($ENV:SystemDrive)\programdata\chocolatey\lib" - $IntuneBody.detectionRules[0].fileOrFolderName = "$($AppConfig.PackageName)" + $IntuneBody.detectionRules[0].fileOrFolderName = $PackageName if ($IntuneBody.installCommandLine -match '%') { $IntuneBody.installCommandLine = Get-CIPPTextReplacement -TenantFilter $TenantFilter -Text $IntuneBody.installCommandLine diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddChocoApp.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddChocoApp.ps1 index 948a4af8af0e..7097d005fddf 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddChocoApp.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddChocoApp.ps1 @@ -13,13 +13,32 @@ function Invoke-AddChocoApp { $ChocoApp = $Request.Body + $PackageName = [string]$ChocoApp.PackageName + if ([string]::IsNullOrWhiteSpace($PackageName)) { + $Result = 'PackageName is required.' + Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Warning' + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = @{ 'Results' = $Result } + }) + } + + if (-not [regex]::IsMatch($PackageName, '^[A-Za-z0-9][A-Za-z0-9._-]*$')) { + $Result = "Invalid PackageName '$PackageName'. Allowed characters: letters, numbers, dot, underscore, hyphen." + Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Warning' + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = @{ 'Results' = $Result } + }) + } + $intuneBody = Get-Content 'AddChocoApp\Choco.app.json' | ConvertFrom-Json $AssignTo = $Request.Body.AssignTo -eq 'customGroup' ? $Request.Body.CustomGroup : $Request.Body.AssignTo $intuneBody.description = $ChocoApp.description $intuneBody.displayName = $ChocoApp.ApplicationName $intuneBody.installExperience.runAsAccount = if ($ChocoApp.InstallAsSystem) { 'system' } else { 'user' } $intuneBody.installExperience.deviceRestartBehavior = if ($ChocoApp.DisableRestart) { 'suppress' } else { 'allow' } - $PackageNameArg = ConvertTo-CIPPSafePwshArg -Value ([string]$ChocoApp.PackageName) + $PackageNameArg = ConvertTo-CIPPSafePwshArg -Value $PackageName $intuneBody.installCommandLine = "powershell.exe -ExecutionPolicy Bypass .\Install.ps1 -InstallChoco -Packagename $PackageNameArg" if ($ChocoApp.customrepo) { $CustomRepoArg = ConvertTo-CIPPSafePwshArg -Value ([string]$ChocoApp.CustomRepo) @@ -31,7 +50,7 @@ function Invoke-AddChocoApp { } $intuneBody.UninstallCommandLine = "powershell.exe -ExecutionPolicy Bypass .\Uninstall.ps1 -Packagename $PackageNameArg" $intuneBody.detectionRules[0].path = "$($ENV:SystemDrive)\programdata\chocolatey\lib" - $intuneBody.detectionRules[0].fileOrFolderName = "$($ChocoApp.PackageName)" + $intuneBody.detectionRules[0].fileOrFolderName = $PackageName $AllowedTenants = Test-CIPPAccess -Request $Request -TenantList $Tenants = ($Request.Body.selectedTenants | Where-Object { $AllowedTenants -contains $_.customerId -or $AllowedTenants -contains 'AllTenants' }).defaultDomainName From 2bbcfcc825d0081fe7add434e41ee02ab3dbb468 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Fri, 5 Jun 2026 16:50:55 +0800 Subject: [PATCH 141/202] This endpoint is silly, we are going to manually try paging --- .../Push-ListMailQuarantineAllTenants.ps1 | 9 ++++++++- .../Spamfilter/Invoke-ListMailQuarantine.ps1 | 10 +++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-ListMailQuarantineAllTenants.ps1 b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-ListMailQuarantineAllTenants.ps1 index 34398c9567bd..515d2e0dd00b 100644 --- a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-ListMailQuarantineAllTenants.ps1 +++ b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-ListMailQuarantineAllTenants.ps1 @@ -11,7 +11,14 @@ Write-Host "PowerShell queue trigger function processed work item: $($Tenant.defaultDomainName)" try { - $quarantineMessages = New-ExoRequest -tenantid $domainName -cmdlet 'Get-QuarantineMessage' -cmdParams @{ 'PageSize' = 1000 } | Select-Object -ExcludeProperty *data.type* + $Page = 1 + $PageSize = 1000 + $quarantineMessages = [System.Collections.Generic.List[object]]::new() + do { + $Results = New-ExoRequest -tenantid $domainName -cmdlet 'Get-QuarantineMessage' -cmdParams @{ PageSize = $PageSize; Page = $Page } | Select-Object -ExcludeProperty *data.type* + if ($Results) { $quarantineMessages.AddRange(@($Results)) } + $Page++ + } while (@($Results).Count -eq $PageSize) foreach ($message in $quarantineMessages) { $messageData = @{ QuarantineMessage = [string]($message | ConvertTo-Json -Depth 10 -Compress) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListMailQuarantine.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListMailQuarantine.ps1 index f0ec05dce631..bf523175d64b 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListMailQuarantine.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListMailQuarantine.ps1 @@ -14,7 +14,15 @@ function Invoke-ListMailQuarantine { try { $GraphRequest = if ($TenantFilter -ne 'AllTenants') { - New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-QuarantineMessage' -cmdParams @{ 'PageSize' = 1000 } | Select-Object -ExcludeProperty *data.type* + $Page = 1 + $PageSize = 1000 + $AllMessages = [System.Collections.Generic.List[object]]::new() + do { + $Results = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-QuarantineMessage' -cmdParams @{ PageSize = $PageSize; Page = $Page } | Select-Object -ExcludeProperty *data.type* + if ($Results) { $AllMessages.AddRange(@($Results)) } + $Page++ + } while (@($Results).Count -eq $PageSize) + $AllMessages } else { $Table = Get-CIPPTable -TableName cacheQuarantineMessages $PartitionKey = 'QuarantineMessage' From 90e45eadaac33b4991b18038c78a3a44e161a05e Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Fri, 5 Jun 2026 17:51:43 +0800 Subject: [PATCH 142/202] pass timing to stop queue rerun protection drift --- .../Activity Triggers/Standards/Push-CIPPStandard.ps1 | 2 +- .../Activity Triggers/Standards/Push-CIPPStandardsList.ps1 | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandard.ps1 b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandard.ps1 index 1e52ae35375a..6138066755c9 100644 --- a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandard.ps1 +++ b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandard.ps1 @@ -21,7 +21,7 @@ function Push-CIPPStandard { $API = "$($Standard)_$($Item.TemplateId)" } - $Rerun = Test-CIPPRerun -Type Standard -Tenant $Tenant -API $API + $Rerun = Test-CIPPRerun -Type Standard -Tenant $Tenant -API $API -BaseTime ([int64]$Item.QueuedTime) if ($Rerun) { Write-Information 'Detected rerun. Exiting cleanly' return diff --git a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandardsList.ps1 b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandardsList.ps1 index e59f70ba4a02..b7391b2f7033 100644 --- a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandardsList.ps1 +++ b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandardsList.ps1 @@ -281,6 +281,7 @@ function Push-CIPPStandardsList { Write-Host "Returning $($ComputedStandards.Count) standards for tenant $TenantFilter after filtering." # Return filtered standards + $QueuedTime = [int64](([datetime]::UtcNow) - (Get-Date '1/1/1970')).TotalSeconds $FilteredStandards = $ComputedStandards.Values | ForEach-Object { [PSCustomObject]@{ Tenant = $_.Tenant @@ -288,6 +289,7 @@ function Push-CIPPStandardsList { Settings = $_.Settings TemplateId = $_.TemplateId FunctionName = 'CIPPStandard' + QueuedTime = $QueuedTime } } Write-Host "Sending back $($FilteredStandards.Count) standards" From 3fa0ee6c412cec4e9a6a3dade4befa21dcffc885 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Fri, 5 Jun 2026 18:20:42 +0800 Subject: [PATCH 143/202] Pipe character escaping in names --- .../Public/Tools/Push-ExecGenerateReportBuilderReport.ps1 | 3 +-- .../GenericTests/Identity/Invoke-CippTestGenericTest004.ps1 | 4 ++-- .../GenericTests/Identity/Invoke-CippTestGenericTest005.ps1 | 4 ++-- .../GenericTests/Identity/Invoke-CippTestGenericTest006.ps1 | 4 ++-- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/Modules/CIPPCore/Public/Tools/Push-ExecGenerateReportBuilderReport.ps1 b/Modules/CIPPCore/Public/Tools/Push-ExecGenerateReportBuilderReport.ps1 index 0e7b5e2ad22e..9fa86ba8df6d 100644 --- a/Modules/CIPPCore/Public/Tools/Push-ExecGenerateReportBuilderReport.ps1 +++ b/Modules/CIPPCore/Public/Tools/Push-ExecGenerateReportBuilderReport.ps1 @@ -60,8 +60,7 @@ function Push-ExecGenerateReportBuilderReport { if ($TestResult) { if ($TestResult.TestType -eq 'Custom' -and $TestResult.ResultDataJson -and $TestResult.MarkdownTemplate) { $Block.content = $TestResult.MarkdownTemplate - } - if (-not $Block.content -and $TestResult.ResultMarkdown) { + } elseif ($TestResult.ResultMarkdown) { $Block | Add-Member -NotePropertyName 'content' -NotePropertyValue $TestResult.ResultMarkdown -Force } } diff --git a/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest004.ps1 b/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest004.ps1 index 653390b5d916..e75d05844c4c 100644 --- a/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest004.ps1 +++ b/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest004.ps1 @@ -63,13 +63,13 @@ function Invoke-CippTestGenericTest004 { $DisplayUsers = $Users | Sort-Object DisplayName | Select-Object -First 100 foreach ($User in $DisplayUsers) { - $Name = $User.DisplayName + $Name = ($User.DisplayName -replace '\|', '\|') $Registered = if ($User.MFARegistration -eq $true) { '✅ Yes' } else { '❌ No' } $Methods = if ($User.MFAMethods) { $MethodList = if ($User.MFAMethods -is [string]) { try { ($User.MFAMethods | ConvertFrom-Json) -join ', ' } catch { $User.MFAMethods } } else { ($User.MFAMethods) -join ', ' } - $MethodList -replace 'microsoftAuthenticator', 'Authenticator' -replace 'phoneAuthentication', 'Phone' -replace 'fido2', 'FIDO2' -replace 'softwareOneTimePasscode', 'Software OTP' -replace 'emailAuthentication', 'Email' -replace 'windowsHelloForBusiness', 'Windows Hello' -replace 'temporaryAccessPass', 'Temp Pass' + ($MethodList -replace 'microsoftAuthenticator', 'Authenticator' -replace 'phoneAuthentication', 'Phone' -replace 'fido2', 'FIDO2' -replace 'softwareOneTimePasscode', 'Software OTP' -replace 'emailAuthentication', 'Email' -replace 'windowsHelloForBusiness', 'Windows Hello' -replace 'temporaryAccessPass', 'Temp Pass') -replace '\|', '\|' } else { 'None' } $Protection = if ($User.CoveredByCA -like 'Enforced*') { "Conditional Access" } elseif ($User.CoveredBySD -eq $true) { 'Security Defaults' } diff --git a/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest005.ps1 b/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest005.ps1 index b8f653383985..259d85be5f01 100644 --- a/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest005.ps1 +++ b/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest005.ps1 @@ -46,13 +46,13 @@ function Invoke-CippTestGenericTest005 { $null = $Result.Append("|-------------|----------------|------------|--------------|-----------------|`n") foreach ($Admin in ($Admins | Sort-Object DisplayName)) { - $Name = $Admin.DisplayName + $Name = ($Admin.DisplayName -replace '\|', '\|') $Registered = if ($Admin.MFARegistration -eq $true) { '✅ Yes' } else { '❌ No' } $Methods = if ($Admin.MFAMethods) { $MethodList = if ($Admin.MFAMethods -is [string]) { try { ($Admin.MFAMethods | ConvertFrom-Json) -join ', ' } catch { $Admin.MFAMethods } } else { ($Admin.MFAMethods) -join ', ' } - $MethodList -replace 'microsoftAuthenticator', 'Authenticator' -replace 'phoneAuthentication', 'Phone' -replace 'fido2', 'FIDO2' -replace 'softwareOneTimePasscode', 'Software OTP' -replace 'emailAuthentication', 'Email' -replace 'windowsHelloForBusiness', 'Windows Hello' -replace 'temporaryAccessPass', 'Temp Pass' + ($MethodList -replace 'microsoftAuthenticator', 'Authenticator' -replace 'phoneAuthentication', 'Phone' -replace 'fido2', 'FIDO2' -replace 'softwareOneTimePasscode', 'Software OTP' -replace 'emailAuthentication', 'Email' -replace 'windowsHelloForBusiness', 'Windows Hello' -replace 'temporaryAccessPass', 'Temp Pass') -replace '\|', '\|' } else { 'None' } $Protection = if ($Admin.CoveredByCA -like 'Enforced*') { "Conditional Access" } elseif ($Admin.CoveredBySD -eq $true) { 'Security Defaults' } diff --git a/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest006.ps1 b/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest006.ps1 index 4b47956736b0..612bcc4e677a 100644 --- a/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest006.ps1 +++ b/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest006.ps1 @@ -45,13 +45,13 @@ function Invoke-CippTestGenericTest006 { $DisplayUsers = $StandardUsers | Sort-Object DisplayName | Select-Object -First 100 foreach ($User in $DisplayUsers) { - $Name = $User.DisplayName + $Name = ($User.DisplayName -replace '\|', '\|') $Registered = if ($User.MFARegistration -eq $true) { '✅ Yes' } else { '❌ No' } $Methods = if ($User.MFAMethods) { $MethodList = if ($User.MFAMethods -is [string]) { try { ($User.MFAMethods | ConvertFrom-Json) -join ', ' } catch { $User.MFAMethods } } else { ($User.MFAMethods) -join ', ' } - $MethodList -replace 'microsoftAuthenticator', 'Authenticator' -replace 'phoneAuthentication', 'Phone' -replace 'fido2', 'FIDO2' -replace 'softwareOneTimePasscode', 'Software OTP' -replace 'emailAuthentication', 'Email' -replace 'windowsHelloForBusiness', 'Windows Hello' -replace 'temporaryAccessPass', 'Temp Pass' + ($MethodList -replace 'microsoftAuthenticator', 'Authenticator' -replace 'phoneAuthentication', 'Phone' -replace 'fido2', 'FIDO2' -replace 'softwareOneTimePasscode', 'Software OTP' -replace 'emailAuthentication', 'Email' -replace 'windowsHelloForBusiness', 'Windows Hello' -replace 'temporaryAccessPass', 'Temp Pass') -replace '\|', '\|' } else { 'None' } $Protection = if ($User.CoveredByCA -like 'Enforced*') { "Conditional Access" } elseif ($User.CoveredBySD -eq $true) { 'Security Defaults' } From 2b3a1c818be171adbd6526f1069447a43f1801b0 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Fri, 5 Jun 2026 18:27:17 +0800 Subject: [PATCH 144/202] Fix for manually run standards being excluded from applied standards report page --- .../Tenant/Standards/Invoke-ListStandardsCompare.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2741abc3e183..a85cf546adb2 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 @@ -19,7 +19,7 @@ function Invoke-ListStandardsCompare { $StandardParams = @{} if ($TemplateFilter) { $StandardParams.TemplateId = $TemplateFilter } if ($TenantFilter) { $StandardParams.TenantFilter = $TenantFilter } - $StandardList = Get-CIPPStandards @StandardParams + $StandardList = @(Get-CIPPStandards @StandardParams) + @(Get-CIPPStandards @StandardParams -runManually $true) $ScopedTemplateGuids = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) $ScopedQuarantineNames = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) From 4435ea380685a4bad5a9e7506aba54f0cfc861ae Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 5 Jun 2026 13:17:04 +0200 Subject: [PATCH 145/202] CA expansion for tags --- .../Functions/Get-CIPPTenantAlignment.ps1 | 13 +++++++++- Modules/CIPPCore/Public/Get-CIPPDrift.ps1 | 22 +++++++++++++--- .../Public/Standards/Get-CIPPStandards.ps1 | 11 ++++++-- .../Invoke-listStandardTemplates.ps1 | 25 +++++++++++++++---- 4 files changed, 60 insertions(+), 11 deletions(-) diff --git a/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 b/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 index 94ad93b8a806..4c712c305346 100644 --- a/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 +++ b/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 @@ -80,6 +80,16 @@ function Get-CIPPTenantAlignment { $TemplatesByPackage[$t.Package].Add($t) } } + $CATagTemplates = Get-CIPPAzDataTableEntity @TemplateTable -Filter "PartitionKey eq 'CATemplate'" + $CATemplatesByPackage = @{} + foreach ($t in $CATagTemplates) { + if ($t.Package) { + if (-not $CATemplatesByPackage.ContainsKey($t.Package)) { + $CATemplatesByPackage[$t.Package] = [System.Collections.Generic.List[object]]::new() + } + $CATemplatesByPackage[$t.Package].Add($t) + } + } # Build tenant standards data structure $tenantData = @{} foreach ($Standard in $Standards) { @@ -245,7 +255,8 @@ function Get-CIPPTenantAlignment { Write-Host "Processing CA Tag: $($Tag.value)" $CAActions = if ($CATemplate.action) { $CATemplate.action } else { @() } $CAReportingEnabled = ($CAActions | Where-Object { $_.value -and ($_.value.ToLower() -eq 'report' -or $_.value.ToLower() -eq 'remediate') }).Count -gt 0 - $TagTemplate = $TagTemplates | Where-Object -Property package -EQ $Tag.value + $TagValue = if ($Tag.value) { $Tag.value } else { $Tag } + $TagTemplate = if ($CATemplatesByPackage.ContainsKey($TagValue)) { $CATemplatesByPackage[$TagValue] } else { @() } $TagTemplate | ForEach-Object { $TagStandardId = "standards.ConditionalAccessTemplate.$($_.GUID)" [PSCustomObject]@{ diff --git a/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 b/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 index a4e159bbfbce..a05310ea1025 100644 --- a/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 @@ -65,6 +65,16 @@ function Get-CIPPDrift { # Load CA templates with GUID hashtable $RawCATemplates = Get-CIPPAzDataTableEntity @IntuneTable -Filter "PartitionKey eq 'CATemplate'" $CATemplatesByGuid = @{} + # Build a hashtable indexed by Package for O(1) CA tag lookup + $CATemplatesByPackage = @{} + foreach ($t in $RawCATemplates) { + if ($t.Package) { + if (-not $CATemplatesByPackage.ContainsKey($t.Package)) { + $CATemplatesByPackage[$t.Package] = [System.Collections.Generic.List[object]]::new() + } + $CATemplatesByPackage[$t.Package].Add($t) + } + } $AllCATemplates = foreach ($RawTemplate in $RawCATemplates) { try { $data = $RawTemplate.JSON | ConvertFrom-Json -Depth 100 -ErrorAction SilentlyContinue @@ -279,9 +289,15 @@ function Get-CIPPDrift { if ($Template.TemplateList.value) { $CATemplateIds.Add($Template.TemplateList.value) } - if ($Template.'TemplateList-Tags'.rawData.templates) { - foreach ($TagTemplate in $Template.'TemplateList-Tags'.rawData.templates) { - $CATemplateIds.Add($TagTemplate.GUID) + if ($Template.'TemplateList-Tags') { + $TagValue = if ($Template.'TemplateList-Tags'.value) { $Template.'TemplateList-Tags'.value } else { $null } + if ($TagValue) { + $ResolvedCATagTemplates = if ($CATemplatesByPackage.ContainsKey($TagValue)) { $CATemplatesByPackage[$TagValue] } else { @() } + foreach ($ResolvedTemplate in $ResolvedCATagTemplates) { + if ($ResolvedTemplate.RowKey -and $ResolvedTemplate.RowKey -notin $CATemplateIds) { + $CATemplateIds.Add($ResolvedTemplate.RowKey) + } + } } } } diff --git a/Modules/CIPPCore/Public/Standards/Get-CIPPStandards.ps1 b/Modules/CIPPCore/Public/Standards/Get-CIPPStandards.ps1 index f94b227967a0..4b3dea9eda89 100644 --- a/Modules/CIPPCore/Public/Standards/Get-CIPPStandards.ps1 +++ b/Modules/CIPPCore/Public/Standards/Get-CIPPStandards.ps1 @@ -49,7 +49,7 @@ function Get-CIPPStandards { $IsArray = $StandardValue -is [System.Collections.IEnumerable] -and -not ($StandardValue -is [string]) if ($IsArray) { - $NewArray = foreach ($Item in $StandardValue) { + $NewArray = @(foreach ($Item in $StandardValue) { if ($Item.'TemplateList-Tags'.value) { $HasExpansions = $true $Table = Get-CippTable -tablename 'templates' @@ -60,6 +60,7 @@ function Get-CIPPStandards { } $Filter = "PartitionKey eq '$PartitionKey'" $TemplatesList = Get-CIPPAzDataTableEntity @Table -Filter $Filter | Where-Object -Property package -EQ $Item.'TemplateList-Tags'.value + Write-Information "Expanding $StandardName tag '$($Item.'TemplateList-Tags'.value)' from partition '$PartitionKey': found $(@($TemplatesList).Count) templates" foreach ($TemplateItem in $TemplatesList) { $TemplateJSON = $TemplateItem.JSON | ConvertFrom-Json -Depth 100 -ErrorAction SilentlyContinue @@ -77,8 +78,10 @@ function Get-CIPPStandards { $Item | Add-Member -NotePropertyName TemplateId -NotePropertyValue $Template.GUID -Force $Item } + }) + if ($NewArray.Count -gt 0) { + $ExpandedStandards[$StandardName] = $NewArray } - $ExpandedStandards[$StandardName] = $NewArray } else { if ($StandardValue.'TemplateList-Tags'.value) { $HasExpansions = $true @@ -139,6 +142,7 @@ function Get-CIPPStandards { foreach ($StandardName in $Standards.PSObject.Properties.Name) { $Value = $Standards.$StandardName + if ($null -eq $Value) { continue } $IsArray = $Value -is [System.Collections.IEnumerable] -and -not ($Value -is [string]) if ($IsArray) { @@ -282,6 +286,7 @@ function Get-CIPPStandards { foreach ($StandardName in $Standards.PSObject.Properties.Name) { $Value = $Standards.$StandardName + if ($null -eq $Value) { continue } $IsArray = $Value -is [System.Collections.IEnumerable] -and -not ($Value -is [string]) if ($IsArray) { @@ -351,6 +356,7 @@ function Get-CIPPStandards { foreach ($StandardName in $Standards.PSObject.Properties.Name) { $Value = $Standards.$StandardName + if ($null -eq $Value) { continue } $IsArray = $Value -is [System.Collections.IEnumerable] -and -not ($Value -is [string]) if ($IsArray) { @@ -430,6 +436,7 @@ function Get-CIPPStandards { foreach ($StandardName in $Standards.PSObject.Properties.Name) { $Value = $Standards.$StandardName + if ($null -eq $Value) { continue } $IsArray = $Value -is [System.Collections.IEnumerable] -and -not ($Value -is [string]) if ($IsArray) { 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 1cb90266c573..c7977465b7e0 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 @@ -45,13 +45,28 @@ function Invoke-listStandardTemplates { $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 + $PartitionKey = switch ($StandardName) { + 'ConditionalAccessTemplate' { 'CATemplate' } + 'IntuneTemplate' { 'IntuneTemplate' } + default { 'IntuneTemplate' } + } + if ($PartitionKey -eq 'CATemplate') { + if (-not $CATemplatesCache) { + $CATable = Get-CippTable -tablename 'templates' + $CAFilter = "PartitionKey eq 'CATemplate'" + $CATemplatesCache = Get-CIPPAzDataTableEntity @CATable -Filter $CAFilter + } + $TemplatesCache = $CATemplatesCache + } else { + if (-not $IntuneTemplatesCache) { + $IntuneTable = Get-CippTable -tablename 'templates' + $IntuneFilter = "PartitionKey eq 'IntuneTemplate'" + $IntuneTemplatesCache = Get-CIPPAzDataTableEntity @IntuneTable -Filter $IntuneFilter + } + $TemplatesCache = $IntuneTemplatesCache } $PackageName = $Item.'TemplateList-Tags'.value - $LiveExpanded = @($IntuneTemplatesCache | Where-Object package -EQ $PackageName | ForEach-Object { + $LiveExpanded = @($TemplatesCache | Where-Object package -EQ $PackageName | ForEach-Object { $TplJson = $_.JSON | ConvertFrom-Json -ErrorAction SilentlyContinue [pscustomobject]@{ GUID = $_.RowKey From 9cab066385d0b71f0879a0a438e0e6ed4949a698 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 5 Jun 2026 13:17:13 +0200 Subject: [PATCH 146/202] CA expansion for tags --- .../Public/Functions/Get-CIPPTenantAlignment.ps1 | 3 +-- Modules/CIPPCore/Public/Get-CIPPDrift.ps1 | 12 ++++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 b/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 index 4c712c305346..ec37099b6951 100644 --- a/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 +++ b/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 @@ -449,8 +449,7 @@ function Get-CIPPTenantAlignment { $DeniedDeviationsCount++ } } - } - elseif ($item.ComplianceStatus -eq 'License Missing') { $LicenseMissingStandards++ } + } elseif ($item.ComplianceStatus -eq 'License Missing') { $LicenseMissingStandards++ } if ($item.ReportingDisabled) { $ReportingDisabledStandardsCount++ } } diff --git a/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 b/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 index a05310ea1025..183325edfe0d 100644 --- a/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 @@ -412,12 +412,12 @@ function Get-CIPPDrift { if (-not $ExistingDriftStates.ContainsKey($Deviation.standardName)) { $RowKey = $Deviation.standardName -replace '\.', '_' $NewDriftEntities.Add(@{ - PartitionKey = $TenantFilter - RowKey = $RowKey - StandardName = $Deviation.standardName - Status = 'New' - LastModified = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') - }) + PartitionKey = $TenantFilter + RowKey = $RowKey + StandardName = $Deviation.standardName + Status = 'New' + LastModified = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') + }) } } if ($NewDriftEntities.Count -gt 0) { From 2b093c37ad3444f546efec5ad157270e7b540ba9 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 5 Jun 2026 13:36:39 +0200 Subject: [PATCH 147/202] fixes issue with CA compare and a weird blank line --- .../Invoke-CIPPStandardConditionalAccessTemplate.ps1 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 index 549396edce9b..047ddd19fc4e 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 @@ -141,7 +141,9 @@ function Invoke-CIPPStandardConditionalAccessTemplate { return } if (!$Compare) { - Set-CIPPStandardsCompareField -FieldName "standards.ConditionalAccessTemplate.$($Settings.TemplateList.value)" -FieldValue $true -Tenant $Tenant + $ExpectedValue = @{ 'Differences' = 'No Differences found' } + $CurrentValue = @{ 'Differences' = 'No Differences found' } + Set-CIPPStandardsCompareField -FieldName "standards.ConditionalAccessTemplate.$($Settings.TemplateList.value)" -FieldValue $true -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } else { #this can still be prettified but is for later. $ExpectedValue = @{ 'Differences' = @() } From f7f51cc192a3e11238deef55a38ed6e43dfaa076 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Fri, 5 Jun 2026 21:00:58 +0800 Subject: [PATCH 148/202] remove duplicate non gated cache collection items --- .../Activity Triggers/Push-CIPPDBCacheData.ps1 | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 index 625eff841975..074278d77cbb 100644 --- a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 +++ b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 @@ -104,24 +104,6 @@ function Push-CIPPDBCacheData { QueueName = "DB Cache Graph - $TenantFilter" }) - # SharePoint config + site data - $Tasks.Add(@{ - FunctionName = 'ExecCIPPDBCache' - CollectionType = 'SharePoint' - TenantFilter = $TenantFilter - QueueId = $QueueId - QueueName = "DB Cache SharePoint - $TenantFilter" - }) - - # Teams config + usage data - $Tasks.Add(@{ - FunctionName = 'ExecCIPPDBCache' - CollectionType = 'Teams' - TenantFilter = $TenantFilter - QueueId = $QueueId - QueueName = "DB Cache Teams - $TenantFilter" - }) - # MFAState runs as its own activity — it makes 6+ API calls, bulk group/role member # resolution, and O(users × policies) CPU work that can take minutes on large tenants $Tasks.Add(@{ From ed47810fdf281fc4366eb76fea9fb843c8cdd108 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Fri, 5 Jun 2026 21:47:33 +0800 Subject: [PATCH 149/202] Update Test-CIPPAccess.ps1 --- Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 b/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 index 06c728084971..531641db5791 100644 --- a/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 +++ b/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 @@ -241,7 +241,7 @@ function Test-CIPPAccess { } } - if ($Permissions -contains 'CIPP.AppSettings.ReadWrite' -and $env:CIPPNG -ne 'true' -and $env:CIPP_SSO_MIGRATION_PROMPT -eq 'true') { + if ($env:CIPPNG -ne 'true') { try { $SSOTable = Get-CIPPTable -tablename 'SSOMigration' $SSOMigration = Get-CIPPAzDataTableEntity @SSOTable -Filter "PartitionKey eq 'SSO' and RowKey eq 'MigrationConfig'" -ErrorAction SilentlyContinue From c040658ade0890d26514364d2d1f7ff5a4b528fe Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Fri, 5 Jun 2026 21:59:25 +0800 Subject: [PATCH 150/202] Update FeatureFlags.json --- Config/FeatureFlags.json | 1 - 1 file changed, 1 deletion(-) diff --git a/Config/FeatureFlags.json b/Config/FeatureFlags.json index f7783e114184..ba991b67302a 100644 --- a/Config/FeatureFlags.json +++ b/Config/FeatureFlags.json @@ -38,7 +38,6 @@ ], "Pages": [ "/cipp/advanced/super-admin/cipp-users", - "/cipp/advanced/super-admin/sso", "/cipp/advanced/super-admin/container", "/cipp/advanced/container-logs", "/cipp/advanced/worker-health" From 612ba54ca20e033be1dd5977efd3720f57b97f18 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Fri, 5 Jun 2026 22:44:44 +0800 Subject: [PATCH 151/202] Update Get-CIPPTenantAlignment.ps1 --- .../Public/Functions/Get-CIPPTenantAlignment.ps1 | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 b/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 index ec37099b6951..0c59d0696f0d 100644 --- a/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 +++ b/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 @@ -108,7 +108,8 @@ function Get-CIPPTenantAlignment { } } - if ($Tenant -and -not $tenantData.ContainsKey($Tenant)) { + if (-not $Tenant) { continue } + if (-not $tenantData.ContainsKey($Tenant)) { $tenantData[$Tenant] = @{} } $tenantData[$Tenant][$FieldName] = @{ @@ -224,7 +225,8 @@ function Get-CIPPTenantAlignment { foreach ($Tag in $IntuneTemplate.'TemplateList-Tags') { $IntuneActions = if ($IntuneTemplate.action) { $IntuneTemplate.action } else { @() } $IntuneReportingEnabled = ($IntuneActions | Where-Object { $_.value -and ($_.value.ToLower() -eq 'report' -or $_.value.ToLower() -eq 'remediate') }).Count -gt 0 - $TagTemplate = if ($TemplatesByPackage.ContainsKey($Tag.value)) { $TemplatesByPackage[$Tag.value] } else { @() } + $TagValue = if ($Tag.value) { $Tag.value } else { $Tag } + $TagTemplate = if ($TagValue -and $TemplatesByPackage.ContainsKey($TagValue)) { $TemplatesByPackage[$TagValue] } else { @() } $TagTemplate | ForEach-Object { $TagStandardId = "standards.IntuneTemplate.$($_.GUID)" [PSCustomObject]@{ @@ -289,8 +291,10 @@ function Get-CIPPTenantAlignment { } } - $AllStandards = $StandardsData.StandardId - $AllStandardsArray = @($AllStandards) + if (-not $StandardsData) { continue } + $AllStandards = @($StandardsData.StandardId | Where-Object { $_ }) + if ($AllStandards.Count -eq 0) { continue } + $AllStandardsArray = $AllStandards $ReportingDisabledStandards = ($StandardsData | Where-Object { -not $_.ReportingEnabled }).StandardId $ReportingDisabledSet = [System.Collections.Generic.HashSet[string]]::new() foreach ($item in $ReportingDisabledStandards) { [void]$ReportingDisabledSet.Add($item) } From f0f361d4b40b9bbab1116eb4aeb8c781afca57f1 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Fri, 5 Jun 2026 23:28:22 +0800 Subject: [PATCH 152/202] restart helper --- .../Authentication/Initialize-CIPPAuth.ps1 | 8 ++-- .../Start-ContainerUpdateCheck.ps1 | 2 +- .../Public/Functions/Request-CIPPRestart.ps1 | 38 +++++++++++++++++++ .../Invoke-ExecContainerManagement.ps1 | 4 +- .../CIPP/Setup/Invoke-ExecSSOSetup.ps1 | 2 +- 5 files changed, 46 insertions(+), 8 deletions(-) create mode 100644 Modules/CIPPCore/Public/Functions/Request-CIPPRestart.ps1 diff --git a/Modules/CIPPCore/Public/Authentication/Initialize-CIPPAuth.ps1 b/Modules/CIPPCore/Public/Authentication/Initialize-CIPPAuth.ps1 index 9974d0a4a6be..78319c3a05b8 100644 --- a/Modules/CIPPCore/Public/Authentication/Initialize-CIPPAuth.ps1 +++ b/Modules/CIPPCore/Public/Authentication/Initialize-CIPPAuth.ps1 @@ -88,7 +88,7 @@ function Initialize-CIPPAuth { Write-Information "[Auth-Init] EasyAuth clientId ($ConfiguredAppId) differs from migration app — migration complete, cleaning up" $Removed = Remove-CIPPMigrationAppSetting -SettingName 'CIPP_SSO_MIGRATION_APPID' if ($Removed) { - [Craft.Services.AppLifecycleBridge]::RequestRestart('SSO migration env var cleaned up during warmup') + Request-CIPPRestart -Reason 'SSO migration env var cleaned up during warmup' } } else { Write-Information '[Auth-Init] No clientId found in EasyAuth config — skipping cleanup' @@ -134,7 +134,7 @@ function Initialize-CIPPAuth { $Configured = Set-CIPPSSOEasyAuth -AppId $ConfiguredAppId -MultiTenant $SSOMultiTenant -TenantId $env:TenantID if ($Configured) { Write-Information '[Auth-Init] EasyAuth issuer updated — requesting container restart' - [Craft.Services.AppLifecycleBridge]::RequestRestart('EasyAuth issuer updated to match SSOMultiTenant setting during warmup') + Request-CIPPRestart -Reason 'EasyAuth issuer updated to match SSOMultiTenant setting during warmup' } } else { Write-Information "[Auth-Init] EasyAuth issuer matches SSOMultiTenant setting ($SSOMultiTenant) — no update needed" @@ -155,7 +155,7 @@ function Initialize-CIPPAuth { $Configured = Set-CIPPSSOEasyAuth -AppId $env:CIPP_SSO_MIGRATION_APPID -MultiTenant $false -TenantId $env:TenantID -UseKvReferences -ImplicitAuth if ($Configured) { Write-Information '[Auth-Init] Implicit auth EasyAuth configured — requesting restart' - [Craft.Services.AppLifecycleBridge]::RequestRestart('Implicit auth EasyAuth configured with central migration app during warmup') + Request-CIPPRestart -Reason 'Implicit auth EasyAuth configured with central migration app during warmup' } } catch { Write-Information "[Auth-Init] Implicit auth EasyAuth setup failed (non-fatal): $_" @@ -186,7 +186,7 @@ function Initialize-CIPPAuth { $Configured = Set-CIPPSSOEasyAuth -AppId $SSOAppId -MultiTenant $SSOMultiTenant -TenantId $env:TenantID -UseKvReferences if ($Configured) { Write-Information '[Auth-Init] EasyAuth configured — requesting container restart' - [Craft.Services.AppLifecycleBridge]::RequestRestart('EasyAuth configured from SSO credentials during warmup') + Request-CIPPRestart -Reason 'EasyAuth configured from SSO credentials during warmup' } } else { Write-Information '[Auth-Init] SAM credentials loaded but no SSO AppId found — enabling setup wizard' diff --git a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-ContainerUpdateCheck.ps1 b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-ContainerUpdateCheck.ps1 index 381abbbc023c..82fe3c739d2b 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-ContainerUpdateCheck.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-ContainerUpdateCheck.ps1 @@ -176,7 +176,7 @@ function Start-ContainerUpdateCheck { if ($UpdateAvailable -and $Settings.AutoUpdate -eq 'true') { Write-LogMessage -API 'ContainerUpdateCheck' -message "Auto-update: new container image detected (running: $RunningDigest, remote: $RemoteDigest). Restarting." -sev Info try { - [Craft.Services.AppLifecycleBridge]::RequestRestart('Auto-update: new container image available') + Request-CIPPRestart -Reason 'Auto-update: new container image available' } catch { Write-LogMessage -API 'ContainerUpdateCheck' -message 'Auto-restart requested but AppLifecycleBridge is not available' -sev Warning } diff --git a/Modules/CIPPCore/Public/Functions/Request-CIPPRestart.ps1 b/Modules/CIPPCore/Public/Functions/Request-CIPPRestart.ps1 new file mode 100644 index 000000000000..8a3196b268a3 --- /dev/null +++ b/Modules/CIPPCore/Public/Functions/Request-CIPPRestart.ps1 @@ -0,0 +1,38 @@ +function Request-CIPPRestart { + <# + .SYNOPSIS + Requests a graceful application restart. + .DESCRIPTION + Attempts to restart the application using the AppLifecycleBridge for a graceful in-process restart. + Falls back to the Azure ARM REST API if the bridge is unavailable. + .PARAMETER Reason + Log message explaining why the restart was requested. + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Reason + ) + + try { + $Subscription = Get-CIPPAzFunctionAppSubId + $SiteName = $env:WEBSITE_SITE_NAME + $RGName = $env:WEBSITE_RESOURCE_GROUP + if (-not $RGName) { + $Owner = $env:WEBSITE_OWNER_NAME + if ($Owner -match '^(?[^+]+)\+(?[^-]+(?:-[^-]+)*?)(?:-[^-]+webspace(?:-Linux)?)?$') { + $RGName = $Matches.RGName + } + } + if (-not ($Subscription -and $RGName -and $SiteName)) { + throw 'Azure App Service details could not be determined from environment' + } + $restartUrl = "https://management.azure.com/subscriptions/$Subscription/resourceGroups/$RGName/providers/Microsoft.Web/sites/$SiteName/restart?api-version=2024-04-01" + $null = New-CIPPAzRestRequest -Uri $restartUrl -Method POST + } catch { + Write-Information "ARM REST API restart failed, falling back to AppLifecycleBridge: $($_.Exception.Message)" + [Craft.Services.AppLifecycleBridge]::RequestRestart($Reason) + } +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecContainerManagement.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecContainerManagement.ps1 index be842ac61a90..556307f2fc22 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecContainerManagement.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecContainerManagement.ps1 @@ -203,7 +203,7 @@ function Invoke-ExecContainerManagement { $Settings = Get-CIPPAzDataTableEntity @SettingsTable -Filter "PartitionKey eq 'Settings' and RowKey eq 'UpdateConfig'" | Select-Object -First 1 if ($UpdateAvailable -and $Settings.AutoUpdate -eq 'true') { Write-LogMessage -API $APIName -headers $Headers -message "Auto-update: new container image detected (running: $RunningDigest, remote: $RemoteDigest). Restarting." -sev Info - try { [Craft.Services.AppLifecycleBridge]::RequestRestart('Auto-update: new container image available') } catch {} + try { Request-CIPPRestart -Reason 'Auto-update: new container image available' } catch {} $Result = "Update available — container restart initiated (auto-update enabled). Running digest: $RunningDigest, Remote digest: $RemoteDigest" } elseif ($UpdateAvailable) { $Result = "Update available. Running digest: $RunningDigest, Remote digest: $RemoteDigest. Restart the container to apply." @@ -323,7 +323,7 @@ function Invoke-ExecContainerManagement { Write-LogMessage -API $APIName -headers $Headers -message 'Container restart requested by super admin' -sev Info $Body = @{ Results = 'Container restart initiated. The application will be back online shortly.' } try { - [Craft.Services.AppLifecycleBridge]::RequestRestart('Restart requested by super admin via container management page') + Request-CIPPRestart -Reason 'Restart requested by super admin via container management page' } catch { $Body = @{ Results = 'Restart command sent but the bridge is not available. The app may need to be restarted from the Azure Portal.' } } diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecSSOSetup.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecSSOSetup.ps1 index a0d29329799d..076fa3c2610e 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecSSOSetup.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecSSOSetup.ps1 @@ -500,7 +500,7 @@ function Invoke-ExecSSOSetup { Write-LogMessage -API $APIName -headers $Headers -message "SSO migration complete: appId=$AppId, multiTenant=$MultiTenant" -sev Info # Step 6: Restart to apply EasyAuth - [Craft.Services.AppLifecycleBridge]::RequestRestart('SSO migration complete — EasyAuth configured with customer CIPP-SSO app') + Request-CIPPRestart -Reason 'SSO migration complete — EasyAuth configured with customer CIPP-SSO app' $Body = @{ Results = @{ From 961462f346d5b8fe357dc322e550dee95a644232 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 5 Jun 2026 12:05:46 -0400 Subject: [PATCH 153/202] fix: role assignment checks --- .../CIS/Identity/Invoke-CippTestCIS_1_1_1.ps1 | 32 ++++++++-- .../CIS/Identity/Invoke-CippTestCIS_1_1_2.ps1 | 21 +++++-- .../CIS/Identity/Invoke-CippTestCIS_1_1_3.ps1 | 22 +++++-- .../CIS/Identity/Invoke-CippTestCIS_1_1_4.ps1 | 40 +++++++++---- .../Identity/Invoke-CippTestZTNA21782.ps1 | 58 +++++++++++++++---- 5 files changed, 135 insertions(+), 38 deletions(-) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_1.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_1.ps1 index c5d1e600002c..cbbb28662c91 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_1.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_1.ps1 @@ -11,17 +11,37 @@ function Invoke-CippTestCIS_1_1_1 { param($Tenant) try { - $Roles = Get-CIPPTestData -TenantFilter $Tenant -Type 'Roles' - $RoleAssignments = Get-CIPPTestData -TenantFilter $Tenant -Type 'RoleAssignments' + $Roles = Get-CippDbRole -TenantFilter $Tenant -IncludePrivilegedRoles + $RoleAssignmentScheduleInstances = Get-CIPPTestData -TenantFilter $Tenant -Type 'RoleAssignmentScheduleInstances' $Users = Get-CIPPTestData -TenantFilter $Tenant -Type 'Users' - if (-not $Roles -or -not $RoleAssignments -or -not $Users) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_1_1_1' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'Required cache (Roles, RoleAssignments, or Users) not found. Please refresh the cache for this tenant.' -Risk 'High' -Name 'Administrative accounts are cloud-only' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Privileged Access' + if ($null -eq $Roles -or $null -eq $Users) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_1_1_1' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'Required cache (Roles or Users) not found. Please refresh the cache for this tenant.' -Risk 'High' -Name 'Administrative accounts are cloud-only' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Privileged Access' return } - $PrivilegedRoleIds = [System.Collections.Generic.HashSet[string]]::new([string[]]$Roles.Where({ $_.isPrivileged -eq $true }).id) - $PrivilegedUserIds = [System.Collections.Generic.HashSet[string]]::new([string[]]($RoleAssignments.Where({ $PrivilegedRoleIds.Contains($_.roleDefinitionId) }).principalId | Select-Object -Unique)) + $PrivilegedRoleIds = [System.Collections.Generic.HashSet[string]]::new() + $PrivilegedUserIds = [System.Collections.Generic.HashSet[string]]::new() + + foreach ($Role in @($Roles)) { + $RoleTemplateId = if ($Role.roleTemplateId) { [string]$Role.roleTemplateId } elseif ($Role.RoletemplateId) { [string]$Role.RoletemplateId } else { $null } + if ($RoleTemplateId) { + [void]$PrivilegedRoleIds.Add($RoleTemplateId) + } + + foreach ($Member in @($Role.members)) { + if ($Member.id) { + [void]$PrivilegedUserIds.Add([string]$Member.id) + } + } + } + + foreach ($Assignment in @($RoleAssignmentScheduleInstances)) { + if ($Assignment.roleDefinitionId -and $Assignment.assignmentType -eq 'Assigned' -and $null -eq $Assignment.endDateTime -and $PrivilegedRoleIds.Contains([string]$Assignment.roleDefinitionId) -and $Assignment.principalId) { + [void]$PrivilegedUserIds.Add([string]$Assignment.principalId) + } + } + $PrivilegedUsers = $Users.Where({ $PrivilegedUserIds.Contains($_.id) }) if (-not $PrivilegedUsers) { diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_2.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_2.ps1 index 910a698a7063..48ff49b3f0f9 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_2.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_2.ps1 @@ -11,11 +11,11 @@ function Invoke-CippTestCIS_1_1_2 { try { $Roles = Get-CIPPTestData -TenantFilter $Tenant -Type 'Roles' - $RoleAssignments = Get-CIPPTestData -TenantFilter $Tenant -Type 'RoleAssignments' + $RoleAssignmentScheduleInstances = Get-CIPPTestData -TenantFilter $Tenant -Type 'RoleAssignmentScheduleInstances' $Users = Get-CIPPTestData -TenantFilter $Tenant -Type 'Users' - if (-not $Roles -or -not $RoleAssignments -or -not $Users) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_1_1_2' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'Required cache (Roles, RoleAssignments, or Users) not found. Please refresh the cache for this tenant.' -Risk 'High' -Name 'Two emergency access accounts have been defined' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Privileged Access' + if ($null -eq $Roles -or $null -eq $Users) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_1_1_2' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'Required cache (Roles or Users) not found. Please refresh the cache for this tenant.' -Risk 'High' -Name 'Two emergency access accounts have been defined' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Privileged Access' return } @@ -25,7 +25,20 @@ function Invoke-CippTestCIS_1_1_2 { return } - $GAUserIds = [System.Collections.Generic.HashSet[string]]::new([string[]]$RoleAssignments.Where({ $_.roleDefinitionId -eq $GA.id }).principalId) + $GAUserIds = [System.Collections.Generic.HashSet[string]]::new() + + foreach ($Member in @($GA.members)) { + if ($Member.id) { + [void]$GAUserIds.Add([string]$Member.id) + } + } + + foreach ($Assignment in @($RoleAssignmentScheduleInstances)) { + if ($Assignment.roleDefinitionId -eq $GA.id -and $Assignment.assignmentType -eq 'Assigned' -and $null -eq $Assignment.endDateTime -and $Assignment.principalId) { + [void]$GAUserIds.Add([string]$Assignment.principalId) + } + } + $GAUsers = $Users.Where({ $GAUserIds.Contains($_.id) }) $BreakGlassPattern = 'breakglass|break-glass|emergency|cipp-bg|bg-admin' $LikelyBG = $GAUsers.Where({ $_.userPrincipalName -match $BreakGlassPattern -and $_.onPremisesSyncEnabled -ne $true }) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_3.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_3.ps1 index d3463e10057c..04f522caaca4 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_3.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_3.ps1 @@ -7,10 +7,10 @@ function Invoke-CippTestCIS_1_1_3 { try { $Roles = Get-CIPPTestData -TenantFilter $Tenant -Type 'Roles' - $RoleAssignments = Get-CIPPTestData -TenantFilter $Tenant -Type 'RoleAssignments' + $RoleAssignmentScheduleInstances = Get-CIPPTestData -TenantFilter $Tenant -Type 'RoleAssignmentScheduleInstances' - if (-not $Roles -or -not $RoleAssignments) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_1_1_3' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'Required cache (Roles or RoleAssignments) not found. Please refresh the cache for this tenant.' -Risk 'High' -Name 'Between two and four global admins are designated' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Privileged Access' + if ($null -eq $Roles) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_1_1_3' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'Required cache (Roles) not found. Please refresh the cache for this tenant.' -Risk 'High' -Name 'Between two and four global admins are designated' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Privileged Access' return } @@ -20,7 +20,21 @@ function Invoke-CippTestCIS_1_1_3 { return } - $GACount = (($RoleAssignments | Where-Object { $_.roleDefinitionId -eq $GA.id }).principalId | Select-Object -Unique).Count + $GAMembers = [System.Collections.Generic.HashSet[string]]::new() + + foreach ($Member in @($GA.members)) { + if ($Member.id) { + [void]$GAMembers.Add([string]$Member.id) + } + } + + foreach ($Assignment in @($RoleAssignmentScheduleInstances)) { + if ($Assignment.roleDefinitionId -eq $GA.id -and $Assignment.assignmentType -eq 'Assigned' -and $null -eq $Assignment.endDateTime -and $Assignment.principalId) { + [void]$GAMembers.Add([string]$Assignment.principalId) + } + } + + $GACount = $GAMembers.Count if ($GACount -ge 2 -and $GACount -le 4) { $Status = 'Passed' diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_4.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_4.ps1 index e33d80beba91..45723d485a42 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_4.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_4.ps1 @@ -7,30 +7,46 @@ function Invoke-CippTestCIS_1_1_4 { try { $Roles = Get-CIPPTestData -TenantFilter $Tenant -Type 'Roles' - $RoleAssignments = Get-CIPPTestData -TenantFilter $Tenant -Type 'RoleAssignments' + $RoleAssignmentScheduleInstances = Get-CIPPTestData -TenantFilter $Tenant -Type 'RoleAssignmentScheduleInstances' $Users = Get-CIPPTestData -TenantFilter $Tenant -Type 'Users' - if (-not $Roles -or -not $RoleAssignments -or -not $Users) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_1_1_4' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'Required cache (Roles, RoleAssignments, or Users) not found. Please refresh the cache for this tenant.' -Risk 'Medium' -Name 'Administrative accounts use licenses with a reduced application footprint' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Privileged Access' + if ($null -eq $Roles -or $null -eq $Users) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_1_1_4' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'Required cache (Roles or Users) not found. Please refresh the cache for this tenant.' -Risk 'Medium' -Name 'Administrative accounts use licenses with a reduced application footprint' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Privileged Access' return } - # SkuPartNumbers that are acceptable for admin accounts: Entra ID P1/P2 only - $AcceptableSkus = @('AAD_PREMIUM', 'AAD_PREMIUM_P2', 'EMS', 'EMSPREMIUM') + $PrivilegedRoleIds = [System.Collections.Generic.HashSet[string]]::new() + $PrivilegedUserIds = [System.Collections.Generic.HashSet[string]]::new() + + foreach ($Role in @($Roles.Where({ $_.isPrivileged -eq $true }))) { + if ($Role.id) { + [void]$PrivilegedRoleIds.Add([string]$Role.id) + } + + foreach ($Member in @($Role.members)) { + if ($Member.id) { + [void]$PrivilegedUserIds.Add([string]$Member.id) + } + } + } + + foreach ($Assignment in @($RoleAssignmentScheduleInstances)) { + if ($Assignment.roleDefinitionId -and $Assignment.assignmentType -eq 'Assigned' -and $null -eq $Assignment.endDateTime -and $PrivilegedRoleIds.Contains([string]$Assignment.roleDefinitionId) -and $Assignment.principalId) { + [void]$PrivilegedUserIds.Add([string]$Assignment.principalId) + } + } - $PrivilegedRoleIds = [System.Collections.Generic.HashSet[string]]::new([string[]]$Roles.Where({ $_.isPrivileged -eq $true }).id) - $PrivilegedUserIds = [System.Collections.Generic.HashSet[string]]::new([string[]]($RoleAssignments.Where({ $PrivilegedRoleIds.Contains($_.roleDefinitionId) }).principalId | Select-Object -Unique)) $PrivilegedUsers = $Users.Where({ $PrivilegedUserIds.Contains($_.id) }) $LicensedAdmins = $PrivilegedUsers.Where({ - $_.assignedLicenses -and $_.assignedLicenses.Count -gt 0 - }) + $_.assignedLicenses -and $_.assignedLicenses.Count -gt 0 + }) $ProductivityServices = [System.Collections.Generic.HashSet[string]]::new([string[]]@('exchange', 'SharePoint', 'MicrosoftCommunicationsOnline', 'TeamspaceAPI')) $NonCompliant = $LicensedAdmins.Where({ - $hasProductivity = $_.assignedPlans.Where({ $ProductivityServices.Contains($_.service) -and $_.capabilityStatus -eq 'Enabled' }, 'First', 1) - [bool]$hasProductivity.Count - }) + $hasProductivity = $_.assignedPlans.Where({ $ProductivityServices.Contains($_.service) -and $_.capabilityStatus -eq 'Enabled' }, 'First', 1) + [bool]$hasProductivity.Count + }) if (-not $LicensedAdmins) { $Status = 'Passed' diff --git a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21782.ps1 b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21782.ps1 index 738136e66f24..8d4ed2870e3b 100644 --- a/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21782.ps1 +++ b/Modules/CIPPTests/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21782.ps1 @@ -7,20 +7,54 @@ function Invoke-CippTestZTNA21782 { try { $UserRegistrationDetails = Get-CIPPTestData -TenantFilter $Tenant -Type 'UserRegistrationDetails' - $RoleAssignments = Get-CIPPTestData -TenantFilter $Tenant -Type 'RoleAssignments' + $Roles = Get-CIPPTestData -TenantFilter $Tenant -Type 'Roles' + $RoleAssignmentScheduleInstances = Get-CIPPTestData -TenantFilter $Tenant -Type 'RoleAssignmentScheduleInstances' - if (-not $UserRegistrationDetails -or -not $RoleAssignments) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21782' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Privileged accounts have phishing-resistant methods registered' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Privileged Access' + if ($null -eq $UserRegistrationDetails -or $null -eq $Roles) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21782' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'Required cache (UserRegistrationDetails or Roles) not found. Please refresh the cache for this tenant.' -Risk 'High' -Name 'Privileged accounts have phishing-resistant methods registered' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Privileged Access' return } $PhishResistantMethods = @('passKeyDeviceBound', 'passKeyDeviceBoundAuthenticator', 'windowsHelloForBusiness') - $RoleAssignmentsByPrincipal = $RoleAssignments | Group-Object principalId -AsHashTable -AsString + $PrivilegedRoleIds = [System.Collections.Generic.HashSet[string]]::new() + $RoleNamesById = @{} + foreach ($Role in @($Roles.Where({ $_.isPrivileged -eq $true }))) { + if ($Role.id) { + [void]$PrivilegedRoleIds.Add([string]$Role.id) + $RoleNamesById[[string]$Role.id] = $Role.displayName + } + } + + $PrivilegedPrincipalsById = @{} + foreach ($Role in @($Roles.Where({ $_.isPrivileged -eq $true }))) { + foreach ($Member in @($Role.members)) { + if (-not $Member.id) { continue } + $principalId = [string]$Member.id + if (-not $PrivilegedPrincipalsById.ContainsKey($principalId)) { + $PrivilegedPrincipalsById[$principalId] = [System.Collections.Generic.HashSet[string]]::new() + } + [void]$PrivilegedPrincipalsById[$principalId].Add($Role.displayName) + } + } + + foreach ($Assignment in @($RoleAssignmentScheduleInstances)) { + if ($Assignment.roleDefinitionId -and $Assignment.assignmentType -eq 'Assigned' -and $null -eq $Assignment.endDateTime -and $PrivilegedRoleIds.Contains([string]$Assignment.roleDefinitionId) -and $Assignment.principalId) { + $principalId = [string]$Assignment.principalId + if (-not $PrivilegedPrincipalsById.ContainsKey($principalId)) { + $PrivilegedPrincipalsById[$principalId] = [System.Collections.Generic.HashSet[string]]::new() + } + $roleName = $RoleNamesById[[string]$Assignment.roleDefinitionId] + if ($roleName) { + [void]$PrivilegedPrincipalsById[$principalId].Add($roleName) + } + } + } + $results = [System.Collections.Generic.List[object]]::new() foreach ($user in $UserRegistrationDetails) { - if (-not $RoleAssignmentsByPrincipal.ContainsKey($user.id)) { continue } - $userRoles = $RoleAssignmentsByPrincipal[$user.id] + if (-not $PrivilegedPrincipalsById.ContainsKey($user.id)) { continue } + $userRoles = $PrivilegedPrincipalsById[$user.id] $hasPhishResistant = $false if ($user.methodsRegistered) { foreach ($method in $PhishResistantMethods) { @@ -31,12 +65,12 @@ function Invoke-CippTestZTNA21782 { } } $results.Add([PSCustomObject]@{ - id = $user.id - userDisplayName = $user.userDisplayName - roleDisplayName = ($userRoles.roleDefinitionName -join ', ') - methodsRegistered = $user.methodsRegistered - phishResistantAuthMethod = $hasPhishResistant - }) + id = $user.id + userDisplayName = $user.userDisplayName + roleDisplayName = ($userRoles | Sort-Object | Select-Object -Unique) -join ', ' + methodsRegistered = $user.methodsRegistered + phishResistantAuthMethod = $hasPhishResistant + }) } $totalUserCount = $results.Count From 503eac5bdb6322f1e42b8e90cc48ab9f3b4c4c5b Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 5 Jun 2026 12:18:30 -0400 Subject: [PATCH 154/202] fix: apps and services test --- .../Public/DBCache/Set-CIPPDBCacheSettings.ps1 | 10 ++++++++++ .../CIS/Identity/Invoke-CippTestCIS_1_3_4.ps1 | 14 ++++++-------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheSettings.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheSettings.ps1 index ce2c02bfa7de..fb28c690c707 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheSettings.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheSettings.ps1 @@ -23,6 +23,16 @@ function Set-CIPPDBCacheSettings { if (!$Settings) { $Settings = @() } Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Settings' -Data $Settings -AddCount $Settings = $null + + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching apps and services settings' -sev Debug + $AppsAndServices = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/admin/appsAndServices' -tenantid $TenantFilter + if ($AppsAndServices -and $AppsAndServices.PSObject.Properties.Name -contains 'settings') { + $AppsAndServices = $AppsAndServices.settings + } + if (!$AppsAndServices) { $AppsAndServices = @() } + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'AppsAndServices' -Data $AppsAndServices -AddCount + $AppsAndServices = $null + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached directory settings successfully' -sev Debug } catch { diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_4.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_4.ps1 index e5a3e2f9dce7..6f3b84e90c14 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_4.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_4.ps1 @@ -6,17 +6,15 @@ function Invoke-CippTestCIS_1_3_4 { param($Tenant) try { - $Settings = Get-CIPPTestData -TenantFilter $Tenant -Type 'Settings' + $AppsAndServices = Get-CIPPTestData -TenantFilter $Tenant -Type 'AppsAndServices' - if (-not $Settings) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_1_3_4' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'Settings cache not found. Please refresh the cache for this tenant.' -Risk 'Medium' -Name "'User owned apps and services' is restricted" -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Application Management' - return + if (-not $AppsAndServices) { + $Settings = Get-CIPPTestData -TenantFilter $Tenant -Type 'Settings' + $AppsAndServices = $Settings | Where-Object { $_.id -eq 'appsAndServices' -or $_.PSObject.Properties.Name -contains 'isOfficeStoreEnabled' } | Select-Object -First 1 } - $AppsAndServices = $Settings | Where-Object { $_.id -eq 'appsAndServices' -or $_.PSObject.Properties.Name -contains 'isOfficeStoreEnabled' } | Select-Object -First 1 - - if (-not $AppsAndServices) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_1_3_4' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'appsAndServices settings not present in the Settings cache.' -Risk 'Medium' -Name "'User owned apps and services' is restricted" -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Application Management' + if (-not $AppsAndServices -or $AppsAndServices.PSObject.Properties.Name -notcontains 'isOfficeStoreEnabled' -or $AppsAndServices.PSObject.Properties.Name -notcontains 'isAppAndServicesTrialEnabled') { + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_1_3_4' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'appsAndServices settings not present in cache. Please refresh the AppsAndServices (or Settings) cache for this tenant.' -Risk 'Medium' -Name "'User owned apps and services' is restricted" -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Application Management' return } From ee1884f7cc5e32fe77ae90d35028f5dfc0761e23 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 5 Jun 2026 12:22:09 -0400 Subject: [PATCH 155/202] add permissions for new settings endpoint --- Config/SAMManifest.json | 117 ++++++++++++++++++++++++---------------- 1 file changed, 71 insertions(+), 46 deletions(-) diff --git a/Config/SAMManifest.json b/Config/SAMManifest.json index ab325ce85ae1..bcba2d5ec5b2 100644 --- a/Config/SAMManifest.json +++ b/Config/SAMManifest.json @@ -10,20 +10,7 @@ "http://localhost:8400" ] }, - "servicePrincipalLockConfiguration": { - "isEnabled": true, - "allProperties": true - }, "requiredResourceAccess": [ - { - "resourceAppId": "c5393580-f805-4401-95e8-94b7a6ef2fc2", - "resourceAccess": [ - { - "id": "594c1fb6-4f81-4475-ae41-0c394909246c", - "type": "Scope" - } - ] - }, { "resourceAppId": "aeb86249-8ea3-49e2-900b-54cc8e308f85", "resourceAccess": [ @@ -48,6 +35,10 @@ "id": "5e1e9171-754d-478c-812c-f1755a9a4c2d", "type": "Role" }, + { + "id": "57f1cf28-c0c4-4ec3-9a30-19a2eaaf2f6e", + "type": "Role" + }, { "id": "f3a65bd4-b703-46df-8f7e-0174fea562aa", "type": "Role" @@ -60,6 +51,10 @@ "id": "35930dcf-aceb-4bd1-b99a-8ffed403c974", "type": "Role" }, + { + "id": "c8948c23-e66b-42db-83fd-770b71ab78d2", + "type": "Role" + }, { "id": "cac88765-0581-4025-9725-5ebc13f729ee", "type": "Role" @@ -92,10 +87,6 @@ "id": "9255e99d-faf5-445e-bbf7-cb71482737c4", "type": "Role" }, - { - "id": "8b9d79d0-ad75-4566-8619-f7500ecfcebe", - "type": "Scope" - }, { "id": "5ac13192-7ace-4fcf-b828-1a26f28068ee", "type": "Role" @@ -116,6 +107,10 @@ "id": "75359482-378d-4052-8f01-80520e7db3cd", "type": "Role" }, + { + "id": "2d9bd318-b883-40be-9df7-63ec4fcdc424", + "type": "Role" + }, { "id": "bf7b1a76-6e77-406b-b258-bf5c7720e98f", "type": "Role" @@ -224,6 +219,14 @@ "id": "4437522e-9a86-4a41-a7da-e380edd4a97d", "type": "Role" }, + { + "id": "0a42382f-155c-4eb1-9bdc-21548ccaa387", + "type": "Role" + }, + { + "id": "a94a502d-0281-4d15-8cd2-682ac9362c4c", + "type": "Role" + }, { "id": "741f803b-c850-494e-b5df-cde7c675a1ca", "type": "Role" @@ -232,6 +235,10 @@ "id": "50483e42-d915-4231-9639-7fdb7fd190e5", "type": "Role" }, + { + "id": "d72bdbf4-a59b-405c-8b04-5995895819ac", + "type": "Role" + }, { "id": "bdfbf15f-ee85-4955-8675-146e8e5296b5", "type": "Scope" @@ -332,6 +339,10 @@ "id": "0c5e8a55-87a6-4556-93ab-adc52c4d862d", "type": "Scope" }, + { + "id": "8b9d79d0-ad75-4566-8619-f7500ecfcebe", + "type": "Scope" + }, { "id": "662ed50a-ac44-4eef-ad86-62eed9be2a29", "type": "Scope" @@ -400,6 +411,10 @@ "id": "46ca0847-7e6b-426e-9775-ea810a948356", "type": "Scope" }, + { + "id": "1e9b7a7e-4d64-44ff-acf5-2e9651c1519f", + "type": "Scope" + }, { "id": "346c19ff-3fb2-4e81-87a0-bac9e33990c1", "type": "Scope" @@ -528,6 +543,10 @@ "id": "b98bfd41-87c6-45cc-b104-e2de4f0dafb9", "type": "Scope" }, + { + "id": "424b07a8-1209-4d17-9fe4-9018a93a1024", + "type": "Scope" + }, { "id": "cac97e40-6730-457d-ad8d-4852fddab7ad", "type": "Scope" @@ -551,38 +570,31 @@ { "id": "b7887744-6746-4312-813d-72daeaee7e2d", "type": "Scope" - }, + } + ] + }, + { + "resourceAppId": "fa3d9a0c-3fb0-42cc-9193-47c7ecd2edbd", + "resourceAccess": [ { - "id": "424b07a8-1209-4d17-9fe4-9018a93a1024", + "id": "1cebfa2a-fb4d-419e-b5f9-839b4383e05a", "type": "Scope" - }, - { - "id": "0a42382f-155c-4eb1-9bdc-21548ccaa387", - "type": "Role" - }, - { - "id": "2d9bd318-b883-40be-9df7-63ec4fcdc424", - "type": "Role" - }, + } + ] + }, + { + "resourceAppId": "00000012-0000-0000-c000-000000000000", + "resourceAccess": [ { - "id": "c8948c23-e66b-42db-83fd-770b71ab78d2", + "id": "e23bd57d-bfd5-4906-867f-89fb5ed8cd43", "type": "Role" }, { - "id": "a94a502d-0281-4d15-8cd2-682ac9362c4c", + "id": "7347eb49-7a1a-43c5-8eac-a5cd1d1c7cf0", "type": "Role" }, { - "id": "d72bdbf4-a59b-405c-8b04-5995895819ac", - "type": "Role" - } - ] - }, - { - "resourceAppId": "fa3d9a0c-3fb0-42cc-9193-47c7ecd2edbd", - "resourceAccess": [ - { - "id": "1cebfa2a-fb4d-419e-b5f9-839b4383e05a", + "id": "c9c9a04d-3b66-4ca8-a00c-fca953e2afd3", "type": "Scope" } ] @@ -591,11 +603,11 @@ "resourceAppId": "00000002-0000-0ff1-ce00-000000000000", "resourceAccess": [ { - "id": "dc50a0fb-09a3-484d-be87-e023b12c6440", + "id": "ef54d2bf-783f-4e0f-bca1-3210c0444d99", "type": "Role" }, { - "id": "ef54d2bf-783f-4e0f-bca1-3210c0444d99", + "id": "dc50a0fb-09a3-484d-be87-e023b12c6440", "type": "Role" }, { @@ -603,11 +615,11 @@ "type": "Role" }, { - "id": "ab4f2b77-0b06-4fc1-a9de-02113fc2ab7c", + "id": "bbd1ca91-75e0-4814-ad94-9c5dbbae3415", "type": "Scope" }, { - "id": "bbd1ca91-75e0-4814-ad94-9c5dbbae3415", + "id": "ab4f2b77-0b06-4fc1-a9de-02113fc2ab7c", "type": "Scope" }, { @@ -616,6 +628,15 @@ } ] }, + { + "resourceAppId": "c5393580-f805-4401-95e8-94b7a6ef2fc2", + "resourceAccess": [ + { + "id": "594c1fb6-4f81-4475-ae41-0c394909246c", + "type": "Scope" + } + ] + }, { "resourceAppId": "00000003-0000-0ff1-ce00-000000000000", "resourceAccess": [ @@ -647,5 +668,9 @@ } ] } - ] -} + ], + "servicePrincipalLockConfiguration": { + "isEnabled": true, + "allProperties": true + } +} \ No newline at end of file From d817b6d2815abb63cd81c4ffcc31d5deb15b35a2 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 5 Jun 2026 12:45:31 -0400 Subject: [PATCH 156/202] fix: cis test 1_3_5 use correct forms settings --- .../DBCache/Set-CIPPDBCacheSettings.ps1 | 54 +++++++++++++++++-- .../CIS/Identity/Invoke-CippTestCIS_1_3_5.ps1 | 12 ++--- 2 files changed, 55 insertions(+), 11 deletions(-) diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheSettings.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheSettings.ps1 index fb28c690c707..9c441e084dd1 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheSettings.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheSettings.ps1 @@ -19,20 +19,66 @@ function Set-CIPPDBCacheSettings { try { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching directory settings' -sev Debug - $Settings = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/settings?$top=999' -tenantid $TenantFilter + $BulkRequests = @( + [PSCustomObject]@{ + id = 'settings' + method = 'GET' + url = '/settings?$top=999' + } + [PSCustomObject]@{ + id = 'appsAndServices' + method = 'GET' + url = '/admin/appsAndServices' + } + [PSCustomObject]@{ + id = 'formsSettings' + method = 'GET' + url = '/admin/forms/settings' + } + ) + + $BulkResults = New-GraphBulkRequest -Requests $BulkRequests -tenantid $TenantFilter + + $SettingsResponse = $BulkResults | Where-Object { $_.id -eq 'settings' } | Select-Object -First 1 + $Settings = @() + if ($SettingsResponse -and $SettingsResponse.status -eq 200) { + $Settings = @($SettingsResponse.body.value) + } else { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Settings request failed in bulk response (status: $($SettingsResponse.status))" -sev Warning + } if (!$Settings) { $Settings = @() } Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Settings' -Data $Settings -AddCount $Settings = $null Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching apps and services settings' -sev Debug - $AppsAndServices = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/admin/appsAndServices' -tenantid $TenantFilter - if ($AppsAndServices -and $AppsAndServices.PSObject.Properties.Name -contains 'settings') { - $AppsAndServices = $AppsAndServices.settings + $AppsAndServicesResponse = $BulkResults | Where-Object { $_.id -eq 'appsAndServices' } | Select-Object -First 1 + $AppsAndServices = @() + if ($AppsAndServicesResponse -and $AppsAndServicesResponse.status -eq 200) { + $AppsAndServices = $AppsAndServicesResponse.body + if ($AppsAndServices -and $AppsAndServices.PSObject.Properties.Name -contains 'settings') { + $AppsAndServices = $AppsAndServices.settings + } + } else { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "AppsAndServices request failed in bulk response (status: $($AppsAndServicesResponse.status))" -sev Warning } if (!$AppsAndServices) { $AppsAndServices = @() } Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'AppsAndServices' -Data $AppsAndServices -AddCount $AppsAndServices = $null + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Forms settings' -sev Debug + $FormsSettingsResponse = $BulkResults | Where-Object { $_.id -eq 'formsSettings' } | Select-Object -First 1 + $FormsSettings = @() + if ($FormsSettingsResponse -and $FormsSettingsResponse.status -eq 200) { + $FormsSettings = $FormsSettingsResponse.body + } else { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "FormsSettings request failed in bulk response (status: $($FormsSettingsResponse.status))" -sev Warning + } + if (!$FormsSettings) { $FormsSettings = @() } + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'FormsSettings' -Data $FormsSettings -AddCount + $FormsSettings = $null + + $BulkResults = $null + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached directory settings successfully' -sev Debug } catch { diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_5.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_5.ps1 index 5b4a9f8f90e2..14bc6409918a 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_5.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_5.ps1 @@ -6,17 +6,15 @@ function Invoke-CippTestCIS_1_3_5 { param($Tenant) try { - $Settings = Get-CIPPTestData -TenantFilter $Tenant -Type 'Settings' + $Forms = Get-CIPPTestData -TenantFilter $Tenant -Type 'FormsSettings' - if (-not $Settings) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_1_3_5' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'Settings cache not found. Please refresh the cache for this tenant.' -Risk 'Medium' -Name 'Internal phishing protection for Forms is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Phishing Protection' - return + if (-not $Forms) { + $Settings = Get-CIPPTestData -TenantFilter $Tenant -Type 'Settings' + $Forms = $Settings | Where-Object { $_.PSObject.Properties.Name -contains 'isInOrgFormsPhishingScanEnabled' } | Select-Object -First 1 } - $Forms = $Settings | Where-Object { $_.PSObject.Properties.Name -contains 'isInOrgFormsPhishingScanEnabled' } | Select-Object -First 1 - if (-not $Forms) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_1_3_5' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'Forms phishing scan setting not in cache.' -Risk 'Medium' -Name 'Internal phishing protection for Forms is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Phishing Protection' + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_1_3_5' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'Forms phishing scan setting not in cache. Please refresh FormsSettings cache for this tenant.' -Risk 'Medium' -Name 'Internal phishing protection for Forms is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Phishing Protection' return } From da10bf92a82e9c791d2c32f6dd0b7b057baac067 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sat, 6 Jun 2026 00:46:28 +0200 Subject: [PATCH 157/202] renumber for cis7 --- .../CIS/Identity/Invoke-CippTestCIS_1_1_1.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_1_1_1.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_1_1_2.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_1_1_2.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_1_1_3.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_1_1_3.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_1_1_4.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_1_1_4.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_1_2_1.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_1_2_1.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_1_2_2.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_1_2_2.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_1_3_1.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_1_3_1.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_1_3_2.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_1_3_2.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_1_3_3.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_1_3_3.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_1_3_4.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_1_3_4.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_1_3_5.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_1_3_5.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_1_3_6.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_1_3_6.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_1_3_7.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_1_3_7.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_1_3_8.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_1_3_8.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_1_3_9.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_1_3_9.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_2_1_1.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_2_1_1.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_2_1_10.md | 2 +- .../Identity/Invoke-CippTestCIS_2_1_10.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_2_1_11.md | 4 +- .../Identity/Invoke-CippTestCIS_2_1_11.ps1 | 8 +-- .../CIS/Identity/Invoke-CippTestCIS_2_1_12.md | 2 +- .../Identity/Invoke-CippTestCIS_2_1_12.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_2_1_13.md | 2 +- .../Identity/Invoke-CippTestCIS_2_1_13.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_2_1_14.md | 2 +- .../Identity/Invoke-CippTestCIS_2_1_14.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_2_1_15.md | 2 +- .../Identity/Invoke-CippTestCIS_2_1_15.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_2_1_2.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_2_1_2.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_2_1_3.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_2_1_3.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_2_1_4.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_2_1_4.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_2_1_5.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_2_1_5.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_2_1_6.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_2_1_6.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_2_1_7.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_2_1_7.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_2_1_8.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_2_1_8.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_2_1_9.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_2_1_9.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_2_2_1.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_2_2_1.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_2_4_1.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_2_4_1.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_2_4_2.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_2_4_2.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_2_4_3.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_2_4_3.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_2_4_4.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_2_4_4.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_2_4_5.md | 13 +++++ .../CIS/Identity/Invoke-CippTestCIS_2_4_5.ps1 | 9 ++++ .../CIS/Identity/Invoke-CippTestCIS_3_1_1.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_3_1_1.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_3_2_1.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_3_2_1.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_3_2_2.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_3_2_2.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_3_2_3.md | 11 ++++ .../CIS/Identity/Invoke-CippTestCIS_3_2_3.ps1 | 37 +++++++++++++ .../CIS/Identity/Invoke-CippTestCIS_3_3_1.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_3_3_1.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_4_1.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_4_1.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_4_2.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_4_2.ps1 | 2 +- .../Identity/Invoke-CippTestCIS_5_1_2_1.md | 2 +- .../Identity/Invoke-CippTestCIS_5_1_2_1.ps1 | 2 +- .../Identity/Invoke-CippTestCIS_5_1_2_2.md | 2 +- .../Identity/Invoke-CippTestCIS_5_1_2_2.ps1 | 8 +-- .../Identity/Invoke-CippTestCIS_5_1_2_3.md | 2 +- .../Identity/Invoke-CippTestCIS_5_1_2_3.ps1 | 2 +- .../Identity/Invoke-CippTestCIS_5_1_2_4.md | 2 +- .../Identity/Invoke-CippTestCIS_5_1_2_4.ps1 | 2 +- .../Identity/Invoke-CippTestCIS_5_1_2_5.md | 2 +- .../Identity/Invoke-CippTestCIS_5_1_2_5.ps1 | 2 +- .../Identity/Invoke-CippTestCIS_5_1_2_6.md | 2 +- .../Identity/Invoke-CippTestCIS_5_1_2_6.ps1 | 2 +- .../Identity/Invoke-CippTestCIS_5_1_3_1.md | 6 +-- .../Identity/Invoke-CippTestCIS_5_1_3_1.ps1 | 24 ++++----- .../Identity/Invoke-CippTestCIS_5_1_3_2.md | 10 ++-- .../Identity/Invoke-CippTestCIS_5_1_3_2.ps1 | 26 +--------- .../Identity/Invoke-CippTestCIS_5_1_3_3.md | 13 +++++ .../Identity/Invoke-CippTestCIS_5_1_3_3.ps1 | 9 ++++ .../Identity/Invoke-CippTestCIS_5_1_3_4.md | 11 ++++ .../Identity/Invoke-CippTestCIS_5_1_3_4.ps1 | 40 ++++++++++++++ .../Identity/Invoke-CippTestCIS_5_1_4_1.md | 2 +- .../Identity/Invoke-CippTestCIS_5_1_4_1.ps1 | 2 +- .../Identity/Invoke-CippTestCIS_5_1_4_2.md | 6 +-- .../Identity/Invoke-CippTestCIS_5_1_4_2.ps1 | 8 +-- .../Identity/Invoke-CippTestCIS_5_1_4_3.md | 2 +- .../Identity/Invoke-CippTestCIS_5_1_4_3.ps1 | 2 +- .../Identity/Invoke-CippTestCIS_5_1_4_4.md | 2 +- .../Identity/Invoke-CippTestCIS_5_1_4_4.ps1 | 2 +- .../Identity/Invoke-CippTestCIS_5_1_4_5.md | 2 +- .../Identity/Invoke-CippTestCIS_5_1_4_5.ps1 | 2 +- .../Identity/Invoke-CippTestCIS_5_1_4_6.md | 2 +- .../Identity/Invoke-CippTestCIS_5_1_4_6.ps1 | 2 +- .../Identity/Invoke-CippTestCIS_5_1_5_1.md | 2 +- .../Identity/Invoke-CippTestCIS_5_1_5_1.ps1 | 2 +- .../Identity/Invoke-CippTestCIS_5_1_5_2.md | 2 +- .../Identity/Invoke-CippTestCIS_5_1_5_2.ps1 | 2 +- .../Identity/Invoke-CippTestCIS_5_1_5_3.md | 11 ++++ .../Identity/Invoke-CippTestCIS_5_1_5_3.ps1 | 37 +++++++++++++ .../Identity/Invoke-CippTestCIS_5_1_5_4.md | 11 ++++ .../Identity/Invoke-CippTestCIS_5_1_5_4.ps1 | 46 ++++++++++++++++ .../Identity/Invoke-CippTestCIS_5_1_5_5.md | 11 ++++ .../Identity/Invoke-CippTestCIS_5_1_5_5.ps1 | 37 +++++++++++++ .../Identity/Invoke-CippTestCIS_5_1_5_6.md | 11 ++++ .../Identity/Invoke-CippTestCIS_5_1_5_6.ps1 | 46 ++++++++++++++++ .../Identity/Invoke-CippTestCIS_5_1_6_1.md | 2 +- .../Identity/Invoke-CippTestCIS_5_1_6_1.ps1 | 2 +- .../Identity/Invoke-CippTestCIS_5_1_6_2.md | 2 +- .../Identity/Invoke-CippTestCIS_5_1_6_2.ps1 | 2 +- .../Identity/Invoke-CippTestCIS_5_1_6_3.md | 2 +- .../Identity/Invoke-CippTestCIS_5_1_6_3.ps1 | 2 +- .../Identity/Invoke-CippTestCIS_5_1_8_1.md | 2 +- .../Identity/Invoke-CippTestCIS_5_1_8_1.ps1 | 2 +- .../Identity/Invoke-CippTestCIS_5_2_2_1.md | 2 +- .../Identity/Invoke-CippTestCIS_5_2_2_1.ps1 | 2 +- .../Identity/Invoke-CippTestCIS_5_2_2_10.md | 2 +- .../Identity/Invoke-CippTestCIS_5_2_2_10.ps1 | 2 +- .../Identity/Invoke-CippTestCIS_5_2_2_11.md | 2 +- .../Identity/Invoke-CippTestCIS_5_2_2_11.ps1 | 2 +- .../Identity/Invoke-CippTestCIS_5_2_2_12.md | 2 +- .../Identity/Invoke-CippTestCIS_5_2_2_12.ps1 | 2 +- .../Identity/Invoke-CippTestCIS_5_2_2_13.md | 15 ++++++ .../Identity/Invoke-CippTestCIS_5_2_2_13.ps1 | 43 +++++++++++++++ .../Identity/Invoke-CippTestCIS_5_2_2_14.md | 12 +++++ .../Identity/Invoke-CippTestCIS_5_2_2_14.ps1 | 40 ++++++++++++++ .../Identity/Invoke-CippTestCIS_5_2_2_15.md | 17 ++++++ .../Identity/Invoke-CippTestCIS_5_2_2_15.ps1 | 45 ++++++++++++++++ .../Identity/Invoke-CippTestCIS_5_2_2_16.md | 17 ++++++ .../Identity/Invoke-CippTestCIS_5_2_2_16.ps1 | 47 +++++++++++++++++ .../Identity/Invoke-CippTestCIS_5_2_2_17.md | 16 ++++++ .../Identity/Invoke-CippTestCIS_5_2_2_17.ps1 | 39 ++++++++++++++ .../Identity/Invoke-CippTestCIS_5_2_2_2.md | 2 +- .../Identity/Invoke-CippTestCIS_5_2_2_2.ps1 | 2 +- .../Identity/Invoke-CippTestCIS_5_2_2_3.md | 2 +- .../Identity/Invoke-CippTestCIS_5_2_2_3.ps1 | 2 +- .../Identity/Invoke-CippTestCIS_5_2_2_4.md | 2 +- .../Identity/Invoke-CippTestCIS_5_2_2_4.ps1 | 2 +- .../Identity/Invoke-CippTestCIS_5_2_2_5.md | 2 +- .../Identity/Invoke-CippTestCIS_5_2_2_5.ps1 | 2 +- .../Identity/Invoke-CippTestCIS_5_2_2_6.md | 2 +- .../Identity/Invoke-CippTestCIS_5_2_2_6.ps1 | 2 +- .../Identity/Invoke-CippTestCIS_5_2_2_7.md | 2 +- .../Identity/Invoke-CippTestCIS_5_2_2_7.ps1 | 2 +- .../Identity/Invoke-CippTestCIS_5_2_2_8.md | 2 +- .../Identity/Invoke-CippTestCIS_5_2_2_8.ps1 | 2 +- .../Identity/Invoke-CippTestCIS_5_2_2_9.md | 2 +- .../Identity/Invoke-CippTestCIS_5_2_2_9.ps1 | 2 +- .../Identity/Invoke-CippTestCIS_5_2_3_1.md | 2 +- .../Identity/Invoke-CippTestCIS_5_2_3_1.ps1 | 2 +- .../Identity/Invoke-CippTestCIS_5_2_3_10.md | 11 ++++ .../Identity/Invoke-CippTestCIS_5_2_3_10.ps1 | 42 +++++++++++++++ .../Identity/Invoke-CippTestCIS_5_2_3_2.md | 2 +- .../Identity/Invoke-CippTestCIS_5_2_3_2.ps1 | 2 +- .../Identity/Invoke-CippTestCIS_5_2_3_3.md | 2 +- .../Identity/Invoke-CippTestCIS_5_2_3_3.ps1 | 2 +- .../Identity/Invoke-CippTestCIS_5_2_3_4.md | 2 +- .../Identity/Invoke-CippTestCIS_5_2_3_4.ps1 | 2 +- .../Identity/Invoke-CippTestCIS_5_2_3_5.md | 2 +- .../Identity/Invoke-CippTestCIS_5_2_3_5.ps1 | 2 +- .../Identity/Invoke-CippTestCIS_5_2_3_6.md | 2 +- .../Identity/Invoke-CippTestCIS_5_2_3_6.ps1 | 2 +- .../Identity/Invoke-CippTestCIS_5_2_3_7.md | 2 +- .../Identity/Invoke-CippTestCIS_5_2_3_7.ps1 | 2 +- .../Identity/Invoke-CippTestCIS_5_2_3_8.md | 11 ++++ .../Identity/Invoke-CippTestCIS_5_2_3_8.ps1 | 39 ++++++++++++++ .../Identity/Invoke-CippTestCIS_5_2_3_9.md | 11 ++++ .../Identity/Invoke-CippTestCIS_5_2_3_9.ps1 | 39 ++++++++++++++ .../Identity/Invoke-CippTestCIS_5_2_4_1.md | 2 +- .../Identity/Invoke-CippTestCIS_5_2_4_1.ps1 | 2 +- .../Identity/Invoke-CippTestCIS_5_2_4_2.md | 13 +++++ .../Identity/Invoke-CippTestCIS_5_2_4_2.ps1 | 9 ++++ .../Identity/Invoke-CippTestCIS_5_2_4_3.md | 13 +++++ .../Identity/Invoke-CippTestCIS_5_2_4_3.ps1 | 9 ++++ .../Identity/Invoke-CippTestCIS_5_2_4_4.md | 13 +++++ .../Identity/Invoke-CippTestCIS_5_2_4_4.ps1 | 9 ++++ .../Identity/Invoke-CippTestCIS_5_2_4_5.md | 13 +++++ .../Identity/Invoke-CippTestCIS_5_2_4_5.ps1 | 9 ++++ .../CIS/Identity/Invoke-CippTestCIS_5_3_1.md | 6 +-- .../CIS/Identity/Invoke-CippTestCIS_5_3_1.ps1 | 52 +++++++++++++++---- .../CIS/Identity/Invoke-CippTestCIS_5_3_2.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_5_3_2.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_5_3_3.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_5_3_3.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_5_3_4.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_5_3_4.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_5_3_5.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_5_3_5.ps1 | 32 +++++++++++- .../CIS/Identity/Invoke-CippTestCIS_6_1_1.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_6_1_1.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_6_1_2.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_6_1_2.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_6_1_3.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_6_1_3.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_6_2_1.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_6_2_1.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_6_2_2.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_6_2_2.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_6_2_3.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_6_2_3.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_6_3_1.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_6_3_1.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_6_3_2.md | 14 +++++ .../CIS/Identity/Invoke-CippTestCIS_6_3_2.ps1 | 32 ++++++++++++ .../CIS/Identity/Invoke-CippTestCIS_6_5_1.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_6_5_1.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_6_5_2.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_6_5_2.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_6_5_3.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_6_5_3.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_6_5_4.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_6_5_4.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_6_5_5.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_6_5_5.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_7_2_1.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_7_2_1.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_7_2_10.md | 2 +- .../Identity/Invoke-CippTestCIS_7_2_10.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_7_2_11.md | 2 +- .../Identity/Invoke-CippTestCIS_7_2_11.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_7_2_2.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_7_2_2.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_7_2_3.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_7_2_3.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_7_2_4.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_7_2_4.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_7_2_5.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_7_2_5.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_7_2_6.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_7_2_6.ps1 | 14 +++-- .../CIS/Identity/Invoke-CippTestCIS_7_2_7.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_7_2_7.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_7_2_8.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_7_2_8.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_7_2_9.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_7_2_9.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_7_3_1.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_7_3_1.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_7_3_2.md | 16 ------ .../CIS/Identity/Invoke-CippTestCIS_7_3_2.ps1 | 36 ------------- .../CIS/Identity/Invoke-CippTestCIS_8_1_1.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_8_1_1.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_8_1_2.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_8_1_2.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_8_2_1.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_8_2_1.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_8_2_2.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_8_2_2.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_8_2_3.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_8_2_3.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_8_2_4.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_8_2_4.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_8_4_1.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_8_4_1.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_8_5_1.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_8_5_1.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_8_5_2.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_8_5_2.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_8_5_3.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_8_5_3.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_8_5_4.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_8_5_4.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_8_5_5.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_8_5_5.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_8_5_6.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_8_5_6.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_8_5_7.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_8_5_7.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_8_5_8.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_8_5_8.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_8_5_9.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_8_5_9.ps1 | 2 +- .../CIS/Identity/Invoke-CippTestCIS_8_6_1.md | 2 +- .../CIS/Identity/Invoke-CippTestCIS_8_6_1.ps1 | 2 +- .../CIPPTests/Public/Tests/CIS/report.json | 28 ++++++++-- 299 files changed, 1317 insertions(+), 380 deletions(-) create mode 100644 Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_4_5.md create mode 100644 Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_4_5.ps1 create mode 100644 Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_3_2_3.md create mode 100644 Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_3_2_3.ps1 create mode 100644 Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_3_3.md create mode 100644 Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_3_3.ps1 create mode 100644 Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_3_4.md create mode 100644 Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_3_4.ps1 create mode 100644 Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_3.md create mode 100644 Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_3.ps1 create mode 100644 Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_4.md create mode 100644 Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_4.ps1 create mode 100644 Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_5.md create mode 100644 Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_5.ps1 create mode 100644 Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_6.md create mode 100644 Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_6.ps1 create mode 100644 Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_13.md create mode 100644 Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_13.ps1 create mode 100644 Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_14.md create mode 100644 Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_14.ps1 create mode 100644 Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_15.md create mode 100644 Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_15.ps1 create mode 100644 Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_16.md create mode 100644 Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_16.ps1 create mode 100644 Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_17.md create mode 100644 Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_17.ps1 create mode 100644 Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_10.md create mode 100644 Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_10.ps1 create mode 100644 Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_8.md create mode 100644 Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_8.ps1 create mode 100644 Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_9.md create mode 100644 Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_9.ps1 create mode 100644 Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_4_2.md create mode 100644 Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_4_2.ps1 create mode 100644 Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_4_3.md create mode 100644 Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_4_3.ps1 create mode 100644 Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_4_4.md create mode 100644 Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_4_4.ps1 create mode 100644 Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_4_5.md create mode 100644 Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_4_5.ps1 create mode 100644 Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_3_2.md create mode 100644 Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_3_2.ps1 delete mode 100644 Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_3_2.md delete mode 100644 Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_3_2.ps1 diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_1.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_1.md index e4dff0042d61..0f6dda714f93 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_1.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_1.md @@ -7,7 +7,7 @@ Microsoft 365 administrators should authenticate using cloud-only identities. Sy 3. Strip productivity licenses from administrative accounts (Entra ID P2 only is sufficient for most cases). **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 1.1.1](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 1.1.1](https://www.cisecurity.org/benchmark/microsoft_365) - [Protect M365 admin accounts](https://learn.microsoft.com/en-us/microsoft-365/admin/add-users/protect-global-admins) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_1.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_1.ps1 index cbbb28662c91..34d40a641d18 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_1.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_1.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_1_1_1 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (1.1.1) - Administrative accounts SHALL be cloud-only + Tests CIS M365 7.0.0 (1.1.1) - Administrative accounts SHALL be cloud-only .DESCRIPTION Privileged role holders should be cloud-only accounts (no onPremisesSyncEnabled), diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_2.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_2.md index 54268d11c7b4..fc11074de984 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_2.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_2.md @@ -9,7 +9,7 @@ If MFA, Conditional Access, or federation outage locks every administrator out o 5. Use a recognisable naming pattern (e.g. `breakglass1@.onmicrosoft.com`). **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 1.1.2](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 1.1.2](https://www.cisecurity.org/benchmark/microsoft_365) - [Manage emergency access accounts](https://learn.microsoft.com/entra/identity/role-based-access-control/security-emergency-access) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_2.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_2.ps1 index 48ff49b3f0f9..4684f72618f0 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_2.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_2.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_1_1_2 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (1.1.2) - At least two emergency access (break-glass) accounts SHALL be defined + Tests CIS M365 7.0.0 (1.1.2) - At least two emergency access (break-glass) accounts SHALL be defined .DESCRIPTION Identifies likely break-glass accounts by looking for cloud-only Global Administrator diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_3.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_3.md index 19c2a16a7e98..756ea075b5fe 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_3.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_3.md @@ -7,7 +7,7 @@ Global Administrator is the most powerful role in Microsoft 365. Too few GA acco 3. Use Privileged Identity Management (PIM) so GA is *eligible*, not *active*, for most accounts. **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 1.1.3](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 1.1.3](https://www.cisecurity.org/benchmark/microsoft_365) - [Microsoft Entra built-in roles](https://learn.microsoft.com/entra/identity/role-based-access-control/permissions-reference) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_3.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_3.ps1 index 04f522caaca4..fe93d5b980b0 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_3.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_3.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_1_1_3 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (1.1.3) - Between two and four global admins SHALL be designated + Tests CIS M365 7.0.0 (1.1.3) - Between two and four global admins SHALL be designated #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_4.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_4.md index b0bd7c393915..63439bbc26da 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_4.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_4.md @@ -7,7 +7,7 @@ Administrative accounts that hold productivity licenses (Exchange Online, ShareP 3. Block sign-in to mailbox / OWA / Teams for these accounts via Conditional Access if licenses cannot be removed. **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 1.1.4](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 1.1.4](https://www.cisecurity.org/benchmark/microsoft_365) - [Tiered admin model](https://learn.microsoft.com/security/privileged-access-workstations/privileged-access-access-model) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_4.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_4.ps1 index 45723d485a42..9c69b5aea1c4 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_4.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_1_4.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_1_1_4 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (1.1.4) - Administrative accounts SHALL use licenses with a reduced application footprint + Tests CIS M365 7.0.0 (1.1.4) - Administrative accounts SHALL use licenses with a reduced application footprint #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_2_1.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_2_1.md index 63ee089c2e32..6e580cac3ebb 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_2_1.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_2_1.md @@ -6,7 +6,7 @@ Public Microsoft 365 groups expose their contents (files, conversations, calenda 2. Set unapproved groups to Private: `Set-UnifiedGroup -Identity -AccessType Private`. **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 1.2.1](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 1.2.1](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_2_1.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_2_1.ps1 index bc3b4679fbad..2b9250d907c5 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_2_1.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_2_1.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_1_2_1 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (1.2.1) - Only organizationally managed/approved public groups SHALL exist + Tests CIS M365 7.0.0 (1.2.1) - Only organizationally managed/approved public groups SHALL exist #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_2_2.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_2_2.md index 5c1aeb5dccfc..6bae75ea3bec 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_2_2.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_2_2.md @@ -6,7 +6,7 @@ Shared mailboxes have a backing user account. If that account is enabled, an att 2. Access shared mailboxes via delegated permissions or SendAs/SendOnBehalf. **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 1.2.2](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 1.2.2](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_2_2.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_2_2.ps1 index c0d3ab48c41e..13ccd0cb2d73 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_2_2.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_2_2.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_1_2_2 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (1.2.2) - Sign-in to shared mailboxes SHALL be blocked + Tests CIS M365 7.0.0 (1.2.2) - Sign-in to shared mailboxes SHALL be blocked #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_1.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_1.md index e185b0977917..1910228a8490 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_1.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_1.md @@ -7,7 +7,7 @@ Update-MgDomain -DomainId -PasswordValidityPeriodInDays 2147483647 -Pas ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 1.3.1](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 1.3.1](https://www.cisecurity.org/benchmark/microsoft_365) - [Set passwords to never expire](https://learn.microsoft.com/microsoft-365/admin/manage/set-password-to-never-expire) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_1.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_1.ps1 index a18099e1a9c7..84d64f46ab84 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_1.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_1.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_1_3_1 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (1.3.1) - Password expiration policy SHALL be set to 'never expire' + Tests CIS M365 7.0.0 (1.3.1) - Password expiration policy SHALL be set to 'never expire' #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_2.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_2.md index c23b86182aea..d8a42ba9bb79 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_2.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_2.md @@ -9,7 +9,7 @@ Create a Conditional Access policy: - Session: Sign-in frequency 3 hours, persistent browser = never **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 1.3.2](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 1.3.2](https://www.cisecurity.org/benchmark/microsoft_365) - [Configure sign-in frequency](https://learn.microsoft.com/entra/identity/conditional-access/concept-session-lifetime) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_2.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_2.ps1 index 5c62307bce01..68883a33dbfc 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_2.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_2.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_1_3_2 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (1.3.2) - Idle session timeout SHALL be 3 hours or less for unmanaged devices + Tests CIS M365 7.0.0 (1.3.2) - Idle session timeout SHALL be 3 hours or less for unmanaged devices #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_3.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_3.md index 645b2677573d..d21021da9488 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_3.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_3.md @@ -7,7 +7,7 @@ Get-SharingPolicy | Set-SharingPolicy -Enabled $false ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 1.3.3](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 1.3.3](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_3.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_3.ps1 index bf9bb37c1c92..6e9ef82f9c03 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_3.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_3.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_1_3_3 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (1.3.3) - 'External sharing' of calendars SHALL NOT be available + Tests CIS M365 7.0.0 (1.3.3) - 'External sharing' of calendars SHALL NOT be available #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_4.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_4.md index a6aa307e1791..27fef94fcf06 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_4.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_4.md @@ -8,7 +8,7 @@ Invoke-MgGraphRequest -Method PATCH -Uri 'https://graph.microsoft.com/beta/admin ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 1.3.4](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 1.3.4](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_4.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_4.ps1 index 6f3b84e90c14..c46886d6a01a 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_4.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_4.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_1_3_4 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (1.3.4) - 'User owned apps and services' SHALL be restricted + Tests CIS M365 7.0.0 (1.3.4) - 'User owned apps and services' SHALL be restricted #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_5.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_5.md index b6bf57a397be..f2de09829984 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_5.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_5.md @@ -5,7 +5,7 @@ Microsoft Forms is regularly abused for credential phishing. Internal phishing s PATCH `https://graph.microsoft.com/beta/admin/forms` with `{ "settings": { "isInOrgFormsPhishingScanEnabled": true } }`. **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 1.3.5](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 1.3.5](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_5.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_5.ps1 index 14bc6409918a..8f2dc4144f9d 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_5.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_5.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_1_3_5 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (1.3.5) - Internal phishing protection for Forms SHALL be enabled + Tests CIS M365 7.0.0 (1.3.5) - Internal phishing protection for Forms SHALL be enabled #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_6.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_6.md index 8818cc84236d..4c5b76f663e8 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_6.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_6.md @@ -7,7 +7,7 @@ Set-OrganizationConfig -CustomerLockBoxEnabled $true ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 1.3.6](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 1.3.6](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_6.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_6.ps1 index e8222109ffd8..c0fa05bb67f2 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_6.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_6.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_1_3_6 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (1.3.6) - Customer Lockbox SHALL be enabled + Tests CIS M365 7.0.0 (1.3.6) - Customer Lockbox SHALL be enabled #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_7.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_7.md index 78c8b4e4212e..bc7981095423 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_7.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_7.md @@ -9,7 +9,7 @@ Update-MgServicePrincipal -ServicePrincipalId -AccountEnabled:$false ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 1.3.7](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 1.3.7](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_7.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_7.ps1 index 8b78b2779132..ff83567b7106 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_7.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_7.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_1_3_7 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (1.3.7) - 'Third-party storage services' SHALL be restricted in 'Microsoft 365 on the web' + Tests CIS M365 7.0.0 (1.3.7) - 'Third-party storage services' SHALL be restricted in 'Microsoft 365 on the web' #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_8.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_8.md index d25727c0730e..4f9fd5621d9f 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_8.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_8.md @@ -6,7 +6,7 @@ Sway can publish content to the public web. CIS recommends restricting external 2. Uncheck "Let people in your organization share their sways with people outside your organization". **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 1.3.8](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 1.3.8](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_8.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_8.ps1 index f6719d06461a..6237f1881e0c 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_8.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_8.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_1_3_8 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (1.3.8) - Sways SHALL NOT be shared with people outside of the organization + Tests CIS M365 7.0.0 (1.3.8) - Sways SHALL NOT be shared with people outside of the organization #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_9.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_9.md index 2d8606ab44c4..589cc87a0507 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_9.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_9.md @@ -15,7 +15,7 @@ Set-OrganizationConfig -BookingsEnabled $false ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 1.3.9](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 1.3.9](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_9.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_9.ps1 index 4314964a0ddf..7438dd19fd01 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_9.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_1_3_9.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_1_3_9 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (1.3.9) - Shared bookings pages SHALL be restricted to select users + Tests CIS M365 7.0.0 (1.3.9) - Shared bookings pages SHALL be restricted to select users #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_1.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_1.md index 627a3a2df691..2039ab2f7a8e 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_1.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_1.md @@ -7,7 +7,7 @@ New-SafeLinksPolicy -Name 'Default Safe Links' -EnableSafeLinksForEmail $true -E ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 2.1.1](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 2.1.1](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_1.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_1.ps1 index eb17dd415e58..545d6281a589 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_1.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_1.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_2_1_1 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (2.1.1) - Safe Links for Office Applications SHALL be enabled + Tests CIS M365 7.0.0 (2.1.1) - Safe Links for Office Applications SHALL be enabled #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_10.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_10.md index 8118a139d999..5a4f88b9050d 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_10.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_10.md @@ -11,7 +11,7 @@ v=DMARC1; p=quarantine; rua=mailto:dmarc-reports@ Move to `p=reject` after monitoring DMARC reports. **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 2.1.10](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 2.1.10](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_10.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_10.ps1 index 3ad101ed07a1..bf59195816f1 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_10.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_10.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_2_1_10 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (2.1.10) - DMARC Records for all Exchange Online domains SHALL be published + Tests CIS M365 7.0.0 (2.1.10) - DMARC Records for all Exchange Online domains SHALL be published #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_11.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_11.md index d5e37d98ee99..cd3e7bf6a98b 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_11.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_11.md @@ -1,11 +1,11 @@ -CIS provides a comprehensive list of ~96 file extensions that should be blocked by the malware filter. The default Microsoft list is much smaller. +CIS v7 provides a comprehensive list of 186 file extensions that should be blocked by the malware filter, and requires at least 90% adoption (roughly 168 extensions) to pass. The default Microsoft list (53 types) is much smaller. **Remediation Action** Use the CIS comprehensive file types list as the `FileTypes` parameter on the malware filter policy. **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 2.1.11](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 2.1.11](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_11.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_11.ps1 index 514175528d8b..26dcfb8d7d14 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_11.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_11.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_2_1_11 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (2.1.11) - Comprehensive attachment filtering SHALL be applied + Tests CIS M365 7.0.0 (2.1.11) - Comprehensive attachment filtering SHALL be applied #> param($Tenant) @@ -16,15 +16,15 @@ function Invoke-CippTestCIS_2_1_11 { $Default = $Malware | Where-Object { $_.IsDefault -eq $true } | Select-Object -First 1 if (-not $Default) { $Default = $Malware | Select-Object -First 1 } - # CIS recommends a minimum of 96 file types (the comprehensive list) + # CIS v7 defines a comprehensive list of 186 extensions and requires at least 90% adoption (>= 168). $FileTypeCount = ($Default.FileTypes | Measure-Object).Count - if ($Default.EnableFileFilter -eq $true -and $FileTypeCount -ge 96) { + if ($Default.EnableFileFilter -eq $true -and $FileTypeCount -ge 168) { $Status = 'Passed' $Result = "Comprehensive attachment filtering is applied — $FileTypeCount file types blocked on '$($Default.Identity)'." } else { $Status = 'Failed' - $Result = "Attachment filter on '$($Default.Identity)' is not comprehensive (EnableFileFilter: $($Default.EnableFileFilter), FileTypes count: $FileTypeCount, expected >= 96)." + $Result = "Attachment filter on '$($Default.Identity)' is not comprehensive (EnableFileFilter: $($Default.EnableFileFilter), FileTypes count: $FileTypeCount, expected >= 168 — 90% of the CIS v7 186-extension list)." } Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_2_1_11' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Comprehensive attachment filtering is applied' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Email Protection' diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_12.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_12.md index 5c1efee024ae..7069f3ac242c 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_12.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_12.md @@ -7,7 +7,7 @@ Set-HostedConnectionFilterPolicy -Identity Default -IPAllowList @() ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 2.1.12](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 2.1.12](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_12.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_12.ps1 index f7faeaa648e1..14cc0b204cff 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_12.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_12.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_2_1_12 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (2.1.12) - Connection filter IP allow list SHALL NOT be used + Tests CIS M365 7.0.0 (2.1.12) - Connection filter IP allow list SHALL NOT be used #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_13.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_13.md index 1990039cb7f4..5a0e5a8fc027 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_13.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_13.md @@ -7,7 +7,7 @@ Set-HostedConnectionFilterPolicy -Identity Default -EnableSafeList $false ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 2.1.13](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 2.1.13](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_13.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_13.ps1 index 4e03bcd89592..45f6889f2edb 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_13.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_13.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_2_1_13 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (2.1.13) - Connection filter safe list SHALL be off + Tests CIS M365 7.0.0 (2.1.13) - Connection filter safe list SHALL be off #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_14.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_14.md index 9fd5737a6fed..321f8ea18700 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_14.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_14.md @@ -7,7 +7,7 @@ Set-HostedContentFilterPolicy -Identity Default -AllowedSenderDomains @() ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 2.1.14](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 2.1.14](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_14.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_14.ps1 index 5a0ae74afa6d..4ba4a8b4dbb0 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_14.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_14.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_2_1_14 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (2.1.14) - Inbound anti-spam policies SHALL NOT contain allowed domains + Tests CIS M365 7.0.0 (2.1.14) - Inbound anti-spam policies SHALL NOT contain allowed domains #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_15.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_15.md index a6106ac41ff4..2ed344eca255 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_15.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_15.md @@ -7,7 +7,7 @@ Set-HostedOutboundSpamFilterPolicy -Identity Default -RecipientLimitExternalPerH ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 2.1.15](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 2.1.15](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_15.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_15.ps1 index b6a4bbb8f8da..b20865319ef6 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_15.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_15.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_2_1_15 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (2.1.15) - Outbound anti-spam message limits SHALL be in place + Tests CIS M365 7.0.0 (2.1.15) - Outbound anti-spam message limits SHALL be in place #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_2.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_2.md index ba645fd493d5..59293fe00069 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_2.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_2.md @@ -7,7 +7,7 @@ Set-MalwareFilterPolicy -Identity Default -EnableFileFilter $true ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 2.1.2](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 2.1.2](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_2.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_2.ps1 index da8173c28853..87b57efd79ca 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_2.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_2.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_2_1_2 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (2.1.2) - Common Attachment Types Filter SHALL be enabled + Tests CIS M365 7.0.0 (2.1.2) - Common Attachment Types Filter SHALL be enabled #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_3.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_3.md index b10b89eca5f0..fed3f1e4721c 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_3.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_3.md @@ -7,7 +7,7 @@ Set-MalwareFilterPolicy -Identity Default -EnableInternalSenderAdminNotification ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 2.1.3](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 2.1.3](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_3.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_3.ps1 index 9e6a04221997..adac4d1ec253 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_3.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_3.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_2_1_3 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (2.1.3) - Notifications for internal users sending malware SHALL be enabled + Tests CIS M365 7.0.0 (2.1.3) - Notifications for internal users sending malware SHALL be enabled #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_4.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_4.md index c13563d32200..6e614fbbc1e2 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_4.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_4.md @@ -8,7 +8,7 @@ New-SafeAttachmentRule -Name 'Default Safe Attachments' -SafeAttachmentPolicy 'D ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 2.1.4](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 2.1.4](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_4.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_4.ps1 index f2635d253a58..935c5ec645b8 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_4.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_4.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_2_1_4 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (2.1.4) - Safe Attachments policy SHALL be enabled + Tests CIS M365 7.0.0 (2.1.4) - Safe Attachments policy SHALL be enabled #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_5.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_5.md index b1cb41752bd4..26adff022e8f 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_5.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_5.md @@ -7,7 +7,7 @@ Set-AtpPolicyForO365 -EnableATPForSPOTeamsODB $true -EnableSafeDocs $true -Allow ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 2.1.5](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 2.1.5](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_5.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_5.ps1 index 121a32a793ab..c5a9c4c94d74 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_5.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_5.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_2_1_5 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (2.1.5) - Safe Attachments for SharePoint, OneDrive, and Microsoft Teams SHALL be enabled + Tests CIS M365 7.0.0 (2.1.5) - Safe Attachments for SharePoint, OneDrive, and Microsoft Teams SHALL be enabled #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_6.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_6.md index fcd3c4ac1143..4d0f6bda873a 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_6.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_6.md @@ -7,7 +7,7 @@ Set-HostedOutboundSpamFilterPolicy -Identity Default -NotifyOutboundSpam $true - ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 2.1.6](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 2.1.6](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_6.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_6.ps1 index c98b48c42893..fbaabf3e4143 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_6.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_6.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_2_1_6 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (2.1.6) - Exchange Online Spam Policies SHALL be set to notify administrators + Tests CIS M365 7.0.0 (2.1.6) - Exchange Online Spam Policies SHALL be set to notify administrators #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_7.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_7.md index c24b59c56e4e..22f278fb75bb 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_7.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_7.md @@ -9,7 +9,7 @@ New-AntiPhishPolicy -Name 'Default Anti-Phishing' -Enabled $true -PhishThreshold ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 2.1.7](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 2.1.7](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_7.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_7.ps1 index 568d6de110d1..5c4fe03bf239 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_7.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_7.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_2_1_7 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (2.1.7) - An anti-phishing policy SHALL be created + Tests CIS M365 7.0.0 (2.1.7) - An anti-phishing policy SHALL be created #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_8.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_8.md index a160398f7b5f..c9c8d90ccc6a 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_8.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_8.md @@ -5,7 +5,7 @@ SPF lists the IPs / hosts authorised to send mail for a domain. Without SPF, att Publish a TXT record at `` with `v=spf1 include:spf.protection.outlook.com -all` (or your appropriate include list). **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 2.1.8](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 2.1.8](https://www.cisecurity.org/benchmark/microsoft_365) - [CIPP Domain Analyser](https://docs.cipp.app/user-documentation/tenant/standards/list-standards/domains-analyser) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_8.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_8.ps1 index b58ffa9664e7..3660a5daea98 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_8.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_8.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_2_1_8 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (2.1.8) - SPF records SHALL be published for all Exchange Domains + Tests CIS M365 7.0.0 (2.1.8) - SPF records SHALL be published for all Exchange Domains #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_9.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_9.md index 19ed1076b68e..43e31c07fa2f 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_9.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_9.md @@ -9,7 +9,7 @@ Set-DkimSigningConfig -Identity -Enabled $true Publish the two CNAME records Microsoft provides before enabling. **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 2.1.9](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 2.1.9](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_9.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_9.ps1 index 17868db8b874..0118728ebe47 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_9.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_1_9.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_2_1_9 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (2.1.9) - DKIM SHALL be enabled for all Exchange Online Domains + Tests CIS M365 7.0.0 (2.1.9) - DKIM SHALL be enabled for all Exchange Online Domains #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_2_1.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_2_1.md index efad5f49a408..8c4b2f1ac670 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_2_1.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_2_1.md @@ -7,7 +7,7 @@ Break-glass accounts must only be used in actual emergencies. Every sign-in need 3. Tabletop the alert path quarterly. **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 2.2.1](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 2.2.1](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_2_1.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_2_1.ps1 index 1ea09c48f151..baf7f4b53db5 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_2_1.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_2_1.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_2_2_1 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (2.2.1) - Emergency access account activity SHALL be monitored + Tests CIS M365 7.0.0 (2.2.1) - Emergency access account activity SHALL be monitored #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_4_1.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_4_1.md index 13a6bdede8a2..53c6a96dd1ed 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_4_1.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_4_1.md @@ -5,7 +5,7 @@ Priority Account Protection applies extra scrutiny to high-value mailboxes (exec Tag executives, finance and IT admins as Priority Accounts in the Microsoft 365 admin centre under **Setup > Priority Accounts**. **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 2.4.1](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 2.4.1](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_4_1.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_4_1.ps1 index 4f9d796d19ba..cf8e928e89bb 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_4_1.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_4_1.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_2_4_1 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (2.4.1) - Priority account protection SHALL be enabled and configured + Tests CIS M365 7.0.0 (2.4.1) - Priority account protection SHALL be enabled and configured #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_4_2.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_4_2.md index 2b649e0c3940..4ff36c2feaae 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_4_2.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_4_2.md @@ -5,7 +5,7 @@ The Strict preset adds the most aggressive Defender for Office settings. Priorit Microsoft 365 Defender > Email & collaboration > Policies & rules > Threat policies > Preset security policies > Strict — toggle to On and scope to Priority Accounts. **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 2.4.2](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 2.4.2](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_4_2.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_4_2.ps1 index d5ce0ee4a7cb..356231e46006 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_4_2.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_4_2.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_2_4_2 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (2.4.2) - Priority accounts SHALL have 'Strict protection' presets applied + Tests CIS M365 7.0.0 (2.4.2) - Priority accounts SHALL have 'Strict protection' presets applied #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_4_3.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_4_3.md index 2ba17cdcbe2b..3cc0c523df54 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_4_3.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_4_3.md @@ -7,7 +7,7 @@ Defender for Cloud Apps is the CASB component of the Microsoft security stack 3. Enable session controls via Conditional Access App Control. **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 2.4.3](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 2.4.3](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_4_3.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_4_3.ps1 index e57072bd622c..76517ca8a2b1 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_4_3.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_4_3.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_2_4_3 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (2.4.3) - Microsoft Defender for Cloud Apps SHALL be enabled and configured + Tests CIS M365 7.0.0 (2.4.3) - Microsoft Defender for Cloud Apps SHALL be enabled and configured #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_4_4.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_4_4.md index 15a0f4919aa0..04a2a21ee598 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_4_4.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_4_4.md @@ -7,7 +7,7 @@ Set-TeamsProtectionPolicy -Identity 'Teams Protection Policy' -ZapEnabled $true ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 2.4.4](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 2.4.4](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_4_4.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_4_4.ps1 index 160a580245a8..cabb10ceabb1 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_4_4.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_4_4.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_2_4_4 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (2.4.4) - Zero-hour auto purge for Microsoft Teams SHALL be on + Tests CIS M365 7.0.0 (2.4.4) - Zero-hour auto purge for Microsoft Teams SHALL be on #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_4_5.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_4_5.md new file mode 100644 index 000000000000..9c41d9c7a0c2 --- /dev/null +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_4_5.md @@ -0,0 +1,13 @@ +Automated Investigation and Response (AIR) automatically investigates alerts, determines whether a threat requires action, and can remediate it without waiting for manual triage. Enabling AIR remediation reduces the time threats remain active and lightens the load on security teams. + +**Remediation Action** + +Microsoft Defender portal > Settings > review the Automated Investigation and Response configuration and ensure remediation actions are enabled (full or semi-automated). + +> Manual control — CIPP reports this as Informational for manual review. + +**Links** +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 2.4.5](https://www.cisecurity.org/benchmark/microsoft_365) + + +%TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_4_5.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_4_5.ps1 new file mode 100644 index 000000000000..39bf0ee60fef --- /dev/null +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_2_4_5.ps1 @@ -0,0 +1,9 @@ +function Invoke-CippTestCIS_2_4_5 { + <# + .SYNOPSIS + Tests CIS M365 7.0.0 (2.4.5) - 'AIR' remediation SHALL be enabled + #> + param($Tenant) + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_2_4_5' -TestType 'Identity' -Status 'Informational' -ResultMarkdown 'This is a manual control. Verify in the Microsoft Defender portal > Settings > Endpoints (or Email & collaboration policies) that Automated Investigation and Response (AIR) remediation is enabled.' -Risk 'Informational' -Name 'AIR remediation is enabled' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Email Protection' +} diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_3_1_1.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_3_1_1.md index c7c69958d432..3d119da323f2 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_3_1_1.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_3_1_1.md @@ -7,7 +7,7 @@ Set-AdminAuditLogConfig -UnifiedAuditLogIngestionEnabled $true ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 3.1.1](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 3.1.1](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_3_1_1.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_3_1_1.ps1 index 64b1e774c244..b27667209ead 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_3_1_1.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_3_1_1.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_3_1_1 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (3.1.1) - Microsoft 365 audit log search SHALL be enabled + Tests CIS M365 7.0.0 (3.1.1) - Microsoft 365 audit log search SHALL be enabled #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_3_2_1.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_3_2_1.md index a98ea0630988..8844e941b485 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_3_2_1.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_3_2_1.md @@ -5,7 +5,7 @@ Microsoft Purview DLP detects and prevents accidental disclosure of sensitive da Use the Purview compliance portal to create at least one DLP policy targeting your sensitive information types and switch the policy to *Turn on*. **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 3.2.1](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 3.2.1](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_3_2_1.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_3_2_1.ps1 index 06cc44371594..0b4dce6b36ec 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_3_2_1.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_3_2_1.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_3_2_1 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (3.2.1) - DLP policies SHALL be enabled + Tests CIS M365 7.0.0 (3.2.1) - DLP policies SHALL be enabled #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_3_2_2.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_3_2_2.md index c819f2f52c0a..bdbbbc8f0236 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_3_2_2.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_3_2_2.md @@ -5,7 +5,7 @@ Sensitive content shared in Teams chats and channels (credit card numbers, NHS n Edit each enforced DLP policy and add `TeamsLocation` so messages and shared files are inspected. **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 3.2.2](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 3.2.2](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_3_2_2.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_3_2_2.ps1 index f7186930a7fc..e0e99e1a52bf 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_3_2_2.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_3_2_2.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_3_2_2 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (3.2.2) - DLP policies SHALL be enabled for Microsoft Teams + Tests CIS M365 7.0.0 (3.2.2) - DLP policies SHALL be enabled for Microsoft Teams #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_3_2_3.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_3_2_3.md new file mode 100644 index 000000000000..02ba8c4b2982 --- /dev/null +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_3_2_3.md @@ -0,0 +1,11 @@ +Microsoft 365 Copilot can retrieve, summarize, and generate content from data the user can access across SharePoint, OneDrive, Teams, and Exchange. Without a DLP policy scoped to Copilot interactions, no technical control prevents sensitive information such as PII, financial data, or health records from being surfaced in Copilot-generated responses. At least one enforced DLP policy must include the Microsoft 365 Copilot and Copilot Chat location so sensitive data is intercepted before it is processed by AI-generated responses. + +**Remediation Action** + +Create or edit a DLP policy in the Microsoft Purview portal to include the Microsoft 365 Copilot and Copilot Chat - All accounts location, add rules for the organization's sensitive information types, and set the policy mode to On (Enable). + +**Links** +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 3.2.3](https://www.cisecurity.org/benchmark/microsoft_365) + + +%TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_3_2_3.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_3_2_3.ps1 new file mode 100644 index 000000000000..d2af4826f188 --- /dev/null +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_3_2_3.ps1 @@ -0,0 +1,37 @@ +function Invoke-CippTestCIS_3_2_3 { + <# + .SYNOPSIS + Tests CIS M365 7.0.0 (3.2.3) - Ensure DLP policies are published for Copilot users + #> + param($Tenant) + + try { + $Dlp = Get-CIPPTestData -TenantFilter $Tenant -Type 'DlpCompliancePolicies' + + if (-not $Dlp) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_3_2_3' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'DlpCompliancePolicies cache not found. Please refresh the cache for this tenant.' -Risk 'Medium' -Name 'DLP policies are published for Copilot users' -UserImpact 'Medium' -ImplementationEffort 'High' -Category 'Data Protection' + return + } + + $CopilotDlp = $Dlp | Where-Object { + $_.Mode -eq 'Enable' -and $_.Enabled -eq $true -and ( + $_.EnforcementPlanes -match 'CopilotExperiences' -or + $_.Workload -match 'Copilot' + ) + } + + if ($CopilotDlp.Count -gt 0) { + $Status = 'Passed' + $Result = "$($CopilotDlp.Count) DLP policy/policies cover Microsoft 365 Copilot:`n`n" + $Result += ($CopilotDlp | ForEach-Object { "- $($_.Name)" }) -join "`n" + } else { + $Status = 'Failed' + $Result = 'No enabled DLP policy currently covers Microsoft 365 Copilot. Add the Microsoft 365 Copilot and Copilot Chat location to at least one enforced DLP policy.' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_3_2_3' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'DLP policies are published for Copilot users' -UserImpact 'Medium' -ImplementationEffort 'High' -Category 'Data Protection' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_3_2_3' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'DLP policies are published for Copilot users' -UserImpact 'Medium' -ImplementationEffort 'High' -Category 'Data Protection' + } +} diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_3_3_1.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_3_3_1.md index b3b863434146..d6e35f8438dc 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_3_3_1.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_3_3_1.md @@ -5,7 +5,7 @@ Sensitivity labels apply visual marking, encryption and access restrictions to d Purview > Information protection > Labels — create at minimum Public / Internal / Confidential / Highly Confidential, then publish under Label policies. **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 3.3.1](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 3.3.1](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_3_3_1.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_3_3_1.ps1 index b5038c4c6a3e..3cc2c00aee38 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_3_3_1.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_3_3_1.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_3_3_1 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (3.3.1) - Information Protection sensitivity label policies SHALL be published + Tests CIS M365 7.0.0 (3.3.1) - Information Protection sensitivity label policies SHALL be published #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_4_1.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_4_1.md index 3e981c1866cb..f0857350fa80 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_4_1.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_4_1.md @@ -7,7 +7,7 @@ Invoke-MgGraphRequest -Method PATCH -Uri 'https://graph.microsoft.com/v1.0/devic ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 4.1](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 4.1](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_4_1.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_4_1.ps1 index 1f11a7bae040..1bb7338e8617 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_4_1.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_4_1.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_4_1 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (4.1) - Devices without a compliance policy SHALL be marked 'not compliant' + Tests CIS M365 7.0.0 (4.1) - Devices without a compliance policy SHALL be marked 'not compliant' #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_4_2.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_4_2.md index f4f82b37b9c6..3501160df0d9 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_4_2.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_4_2.md @@ -5,7 +5,7 @@ Personal device enrollment can give attackers a foothold via lost / stolen / unm Intune > Devices > Enrollment > Device platform restrictions > Default policy — set Personally owned to Block on every platform. **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 4.2](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 4.2](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_4_2.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_4_2.ps1 index 964af9fc3390..db763eb1c15b 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_4_2.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_4_2.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_4_2 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (4.2) - Device enrollment for personally owned devices SHALL be blocked by default + Tests CIS M365 7.0.0 (4.2) - Device enrollment for personally owned devices SHALL be blocked by default #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_1.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_1.md index 6d84bdb2d7b1..f70a61002373 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_1.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_1.md @@ -5,7 +5,7 @@ Per-user MFA (the legacy setting on a user object) bypasses Conditional Access l Use Graph or the legacy `Set-MsolUser` to set `StrongAuthenticationRequirements` to an empty array on every user. **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 5.1.2.1](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.1.2.1](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_1.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_1.ps1 index ed260404ad02..7b0a3e548284 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_1.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_1.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_5_1_2_1 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (5.1.2.1) - 'Per-user MFA' SHALL be disabled + Tests CIS M365 7.0.0 (5.1.2.1) - 'Per-user MFA' SHALL be disabled #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_2.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_2.md index 219ae2f95ce4..26158ebe9169 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_2.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_2.md @@ -7,7 +7,7 @@ Update-MgPolicyAuthorizationPolicy -DefaultUserRolePermissions @{ AllowedToCreat ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 5.1.2.2](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.1.2.2](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_2.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_2.ps1 index b8944fcd9393..1f761994d8a1 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_2.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_2.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_5_1_2_2 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (5.1.2.2) - Third party integrated applications SHALL NOT be allowed + Tests CIS M365 7.0.0 (5.1.2.2) - Users SHALL NOT be able to register applications #> param($Tenant) @@ -9,7 +9,7 @@ function Invoke-CippTestCIS_5_1_2_2 { $Auth = Get-CIPPTestData -TenantFilter $Tenant -Type 'AuthorizationPolicy' if (-not $Auth) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_1_2_2' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'AuthorizationPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'High' -Name 'Third party integrated applications are not allowed' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Application Management' + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_1_2_2' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'AuthorizationPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'High' -Name 'Users cannot register applications' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Application Management' return } @@ -23,9 +23,9 @@ function Invoke-CippTestCIS_5_1_2_2 { $Result = "Users can register applications (allowedToCreateApps: $($Cfg.defaultUserRolePermissions.allowedToCreateApps))." } - Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_1_2_2' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Third party integrated applications are not allowed' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Application Management' + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_1_2_2' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Users cannot register applications' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Application Management' } catch { $ErrorMessage = Get-CippException -Exception $_ - Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_1_2_2' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Third party integrated applications are not allowed' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Application Management' + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_1_2_2' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Users cannot register applications' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Application Management' } } diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_3.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_3.md index b306d32b83de..02cf99a8ae96 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_3.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_3.md @@ -7,7 +7,7 @@ Update-MgPolicyAuthorizationPolicy -DefaultUserRolePermissions @{ AllowedToCreat ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 5.1.2.3](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.1.2.3](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_3.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_3.ps1 index c1da8c124e05..8caf0ef1c7e8 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_3.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_3.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_5_1_2_3 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (5.1.2.3) - 'Restrict non-admin users from creating tenants' SHALL be 'Yes' + Tests CIS M365 7.0.0 (5.1.2.3) - 'Restrict non-admin users from creating tenants' SHALL be 'Yes' #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_4.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_4.md index 5c00773baf9b..54f00c0146ee 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_4.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_4.md @@ -7,7 +7,7 @@ Invoke-MgGraphRequest -Method PATCH -Uri 'https://graph.microsoft.com/beta/admin ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 5.1.2.4](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.1.2.4](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_4.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_4.ps1 index 5c6a1b5ff26a..7a7bd36b3481 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_4.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_4.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_5_1_2_4 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (5.1.2.4) - Access to the Entra admin center SHALL be restricted + Tests CIS M365 7.0.0 (5.1.2.4) - Access to the Entra admin center SHALL be restricted #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_5.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_5.md index 053a01a8bd68..813362763c34 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_5.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_5.md @@ -5,7 +5,7 @@ Microsoft Entra admin centre > User experiences > Company branding > Default sign-in > Show option to remain signed-in: **No**. **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 5.1.2.5](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.1.2.5](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_5.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_5.ps1 index 18a8add13ba3..5ca03056397c 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_5.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_5.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_5_1_2_5 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (5.1.2.5) - The option to remain signed in SHALL be hidden + Tests CIS M365 7.0.0 (5.1.2.5) - The option to remain signed in SHALL be hidden #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_6.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_6.md index 5449ba4cff63..7702d6c5a062 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_6.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_6.md @@ -5,7 +5,7 @@ LinkedIn account connections share user profile information with LinkedIn. CIS r Microsoft 365 admin centre > Settings > Org settings > LinkedIn account connections — set to **Do not allow**. **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 5.1.2.6](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.1.2.6](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_6.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_6.ps1 index e31a7ab6d28e..cd28e8a299c6 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_6.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_2_6.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_5_1_2_6 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (5.1.2.6) - 'LinkedIn account connections' SHALL be disabled + Tests CIS M365 7.0.0 (5.1.2.6) - 'LinkedIn account connections' SHALL be disabled #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_3_1.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_3_1.md index 6aaf0117f3f7..66a23d2b06f6 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_3_1.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_3_1.md @@ -1,13 +1,13 @@ -A dynamic group containing every guest enables guest-aware Conditional Access policies, access reviews and lifecycle automation without manual maintenance. +Security groups grant access to resources. Allowing standard users to create security groups bypasses access governance and is a common privilege escalation path — a compromised user can create deceptively named groups that an administrator later trusts with elevated access or excludes from Conditional Access. **Remediation Action** ```powershell -New-MgGroup -DisplayName 'All Guest Users' -SecurityEnabled:$true -MailEnabled:$false -MailNickname 'allguests' -GroupTypes 'DynamicMembership' -MembershipRule '(user.userType -eq "Guest")' -MembershipRuleProcessingState 'On' +Update-MgPolicyAuthorizationPolicy -DefaultUserRolePermissions @{ AllowedToCreateSecurityGroups = $false } ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 5.1.3.1](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.1.3.1](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_3_1.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_3_1.ps1 index b4538c5aff98..db6795ea6dc7 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_3_1.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_3_1.ps1 @@ -1,35 +1,31 @@ function Invoke-CippTestCIS_5_1_3_1 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (5.1.3.1) - A dynamic group for guest users SHALL be created + Tests CIS M365 7.0.0 (5.1.3.1) - Users SHALL NOT be able to create security groups #> param($Tenant) try { - $Groups = Get-CIPPTestData -TenantFilter $Tenant -Type 'Groups' + $Auth = Get-CIPPTestData -TenantFilter $Tenant -Type 'AuthorizationPolicy' - if (-not $Groups) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_1_3_1' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'Groups cache not found. Please refresh the cache for this tenant.' -Risk 'Medium' -Name 'A dynamic group for guest users is created' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Group Management' + if (-not $Auth) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_1_3_1' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'AuthorizationPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'Medium' -Name 'Users cannot create security groups' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Group Management' return } - $GuestDynamic = $Groups | Where-Object { - $_.groupTypes -contains 'DynamicMembership' -and - $_.membershipRule -match "userType\s*-eq\s*['""]Guest['""]" - } + $Cfg = $Auth | Select-Object -First 1 - if ($GuestDynamic.Count -gt 0) { + if ($Cfg.defaultUserRolePermissions.allowedToCreateSecurityGroups -eq $false) { $Status = 'Passed' - $Result = "$($GuestDynamic.Count) dynamic group(s) target guest users:`n`n" - $Result += ($GuestDynamic | ForEach-Object { "- $($_.displayName)" }) -join "`n" + $Result = 'Users cannot create security groups (allowedToCreateSecurityGroups: false).' } else { $Status = 'Failed' - $Result = 'No dynamic security group targeting `userType -eq "Guest"` was found. Create one so guest-targeted Conditional Access can use it.' + $Result = "Users can create security groups (allowedToCreateSecurityGroups: $($Cfg.defaultUserRolePermissions.allowedToCreateSecurityGroups))." } - Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_1_3_1' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'A dynamic group for guest users is created' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Group Management' + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_1_3_1' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Users cannot create security groups' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Group Management' } catch { $ErrorMessage = Get-CippException -Exception $_ - Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_1_3_1' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'A dynamic group for guest users is created' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Group Management' + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_1_3_1' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Users cannot create security groups' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Group Management' } } diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_3_2.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_3_2.md index 47345a8315e1..b5aee1fb70b6 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_3_2.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_3_2.md @@ -1,13 +1,13 @@ -Security groups grant access to resources. Allowing standard users to create security groups bypasses access governance and is a common privilege escalation path. +By default any authenticated user can browse the My Groups portal (https://myaccount.microsoft.com/groups) and enumerate group memberships, SharePoint/Teams URLs and group email addresses across the tenant — useful reconnaissance for identifying privileged groups and planning lateral movement. Restricting the web interface limits passive enumeration by users who do not need to browse groups. **Remediation Action** -```powershell -Update-MgPolicyAuthorizationPolicy -DefaultUserRolePermissions @{ AllowedToCreateSecurityGroups = $false } -``` +Microsoft Entra admin center > Entra ID > Groups > General > Self Service Group Management > set **Restrict user ability to access groups features in My Groups** to **Yes**. + +> Manual control — no Graph property is exposed, so CIPP reports this as Informational for manual review. **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 5.1.3.2](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.1.3.2](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_3_2.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_3_2.ps1 index b360366b00c9..1162bcee4db7 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_3_2.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_3_2.ps1 @@ -1,31 +1,9 @@ function Invoke-CippTestCIS_5_1_3_2 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (5.1.3.2) - Users SHALL NOT be able to create security groups + Tests CIS M365 7.0.0 (5.1.3.2) - 'Restrict user ability to access groups features in My Groups' SHALL be set to 'Yes' #> param($Tenant) - try { - $Auth = Get-CIPPTestData -TenantFilter $Tenant -Type 'AuthorizationPolicy' - - if (-not $Auth) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_1_3_2' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'AuthorizationPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'Medium' -Name 'Users cannot create security groups' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Group Management' - return - } - - $Cfg = $Auth | Select-Object -First 1 - - if ($Cfg.defaultUserRolePermissions.allowedToCreateSecurityGroups -eq $false) { - $Status = 'Passed' - $Result = 'Users cannot create security groups (allowedToCreateSecurityGroups: false).' - } else { - $Status = 'Failed' - $Result = "Users can create security groups (allowedToCreateSecurityGroups: $($Cfg.defaultUserRolePermissions.allowedToCreateSecurityGroups))." - } - - Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_1_3_2' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Users cannot create security groups' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Group Management' - } catch { - $ErrorMessage = Get-CippException -Exception $_ - Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_1_3_2' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Users cannot create security groups' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Group Management' - } + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_1_3_2' -TestType 'Identity' -Status 'Informational' -ResultMarkdown 'This is a manual control. Verify in the Microsoft Entra admin center > Entra ID > Groups > General that, under Self Service Group Management, "Restrict user ability to access groups features in My Groups" is set to Yes.' -Risk 'Informational' -Name "Restrict user ability to access groups features in My Groups is set to 'Yes'" -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Group Management' } diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_3_3.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_3_3.md new file mode 100644 index 000000000000..38c2e89ee6a5 --- /dev/null +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_3_3.md @@ -0,0 +1,13 @@ +Group owners are standard users who may not understand access-governance requirements. Allowing owners to approve membership requests through My Groups means additions to security or Microsoft 365 groups can occur without administrator review, bypassing formal access provisioning and expanding the blast radius of a compromised account. + +**Remediation Action** + +Microsoft Entra admin center > Entra ID > Groups > General > set **Owners can manage group membership requests in My Groups** to **No**. + +> Manual control — no Graph property is exposed, so CIPP reports this as Informational for manual review. + +**Links** +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.1.3.3](https://www.cisecurity.org/benchmark/microsoft_365) + + +%TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_3_3.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_3_3.ps1 new file mode 100644 index 000000000000..72d18b20fe01 --- /dev/null +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_3_3.ps1 @@ -0,0 +1,9 @@ +function Invoke-CippTestCIS_5_1_3_3 { + <# + .SYNOPSIS + Tests CIS M365 7.0.0 (5.1.3.3) - 'Owners can manage group membership requests in My Groups' SHALL be set to 'No' + #> + param($Tenant) + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_1_3_3' -TestType 'Identity' -Status 'Informational' -ResultMarkdown 'This is a manual control. Verify in the Microsoft Entra admin center > Entra ID > Groups > General that "Owners can manage group membership requests in My Groups" is set to No.' -Risk 'Informational' -Name "Owners can manage group membership requests in My Groups is set to 'No'" -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Group Management' +} diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_3_4.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_3_4.md new file mode 100644 index 000000000000..312a2887f7fa --- /dev/null +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_3_4.md @@ -0,0 +1,11 @@ +By default every user can create Microsoft 365 groups through the Azure portal, API or PowerShell, and the creator becomes owner. Restricting Microsoft 365 group creation to administrators keeps group creation and the resources they grant (Teams, SharePoint, mailboxes) under centralized governance. + +**Remediation Action** + +In the Group.Unified directory settings (templateId `62375ab9-6b52-47ed-826b-58e47e0e304b`) set **EnableGroupCreation** to `false`. If the settings object does not exist, use Microsoft Entra admin center > Entra ID > Groups > General > set **Users can create Microsoft 365 groups in Azure portals, API or PowerShell** to **No** (this creates the object with defaults). + +**Links** +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.1.3.4](https://www.cisecurity.org/benchmark/microsoft_365) + + +%TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_3_4.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_3_4.ps1 new file mode 100644 index 000000000000..91c880253e9a --- /dev/null +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_3_4.ps1 @@ -0,0 +1,40 @@ +function Invoke-CippTestCIS_5_1_3_4 { + <# + .SYNOPSIS + Tests CIS M365 7.0.0 (5.1.3.4) - 'Users can create Microsoft 365 groups in Azure portals, API or PowerShell' SHALL be set to 'No' + #> + param($Tenant) + + try { + $Settings = Get-CIPPTestData -TenantFilter $Tenant -Type 'Settings' + + if (-not $Settings) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_1_3_4' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'Settings cache not found. Please refresh the cache for this tenant.' -Risk 'Medium' -Name 'Users cannot create Microsoft 365 groups in Azure portals, API or PowerShell' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Group Management' + return + } + + # Group.Unified directory settings template + $GroupSetting = $Settings | Where-Object { $_.templateId -eq '62375ab9-6b52-47ed-826b-58e47e0e304b' -or $_.displayName -eq 'Group.Unified' } | Select-Object -First 1 + + if (-not $GroupSetting) { + # No Group.Unified settings object means defaults are in effect (EnableGroupCreation = true), which is non-compliant. + $Status = 'Failed' + $Result = 'No Group.Unified directory settings object exists, so the default applies (users can create Microsoft 365 groups). Set Users can create Microsoft 365 groups to No.' + } else { + $EnableGroupCreation = ($GroupSetting.values | Where-Object { $_.name -eq 'EnableGroupCreation' }).value + + if ("$EnableGroupCreation" -eq 'false') { + $Status = 'Passed' + $Result = 'Users cannot create Microsoft 365 groups (EnableGroupCreation: false).' + } else { + $Status = 'Failed' + $Result = "Users can create Microsoft 365 groups (EnableGroupCreation: $EnableGroupCreation)." + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_1_3_4' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Users cannot create Microsoft 365 groups in Azure portals, API or PowerShell' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Group Management' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_1_3_4' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Users cannot create Microsoft 365 groups in Azure portals, API or PowerShell' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Group Management' + } +} diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_1.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_1.md index c9f17ddb90b5..fb7830936605 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_1.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_1.md @@ -5,7 +5,7 @@ A compromised user can register a rogue device under their identity, satisfy com Microsoft Entra admin centre > Devices > Device settings > Users may join devices to Microsoft Entra: Selected (and add the helpdesk group) or None. **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 5.1.4.1](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.1.4.1](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_1.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_1.ps1 index 173efbf8e0dd..a79d7e793ce5 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_1.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_1.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_5_1_4_1 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (5.1.4.1) - Ability to join devices to Entra SHALL be restricted + Tests CIS M365 7.0.0 (5.1.4.1) - Ability to join devices to Entra SHALL be restricted #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_2.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_2.md index a484d774c1e9..68a562533cb1 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_2.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_2.md @@ -1,11 +1,11 @@ -A high device quota lets a compromised user enroll multiple rogue devices to maintain persistence. CIS recommends 20 or fewer. +A high device quota lets a compromised user enroll multiple rogue devices to maintain persistence. CIS recommends 10 or fewer. **Remediation Action** -Microsoft Entra admin centre > Devices > Device settings > Maximum number of devices per user: 20 (or less). +Microsoft Entra admin centre > Devices > Device settings > Maximum number of devices per user: 10 (or less). **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 5.1.4.2](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.1.4.2](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_2.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_2.ps1 index acecf5f6d85e..edd50ccc8532 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_2.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_2.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_5_1_4_2 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (5.1.4.2) - Maximum number of devices per user SHALL be limited + Tests CIS M365 7.0.0 (5.1.4.2) - Maximum number of devices per user SHALL be limited #> param($Tenant) @@ -16,12 +16,12 @@ function Invoke-CippTestCIS_5_1_4_2 { $Cfg = $DRP | Select-Object -First 1 $Quota = [int]$Cfg.userDeviceQuota - if ($Quota -gt 0 -and $Quota -le 20) { + if ($Quota -gt 0 -and $Quota -le 10) { $Status = 'Passed' - $Result = "userDeviceQuota is set to $Quota (CIS recommends 20 or less)." + $Result = "userDeviceQuota is set to $Quota (CIS recommends 10 or less)." } else { $Status = 'Failed' - $Result = "userDeviceQuota is $Quota (CIS recommends 20 or less)." + $Result = "userDeviceQuota is $Quota (CIS recommends 10 or less)." } Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_1_4_2' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Maximum number of devices per user is limited' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Device Management' diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_3.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_3.md index 3e6a3d68f04a..b1851b425837 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_3.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_3.md @@ -5,7 +5,7 @@ Granting Global Administrator local admin on every Entra-joined device dramatica Microsoft Entra admin centre > Devices > Device settings > Global administrator role is added as local administrator on the device during Microsoft Entra join: **No**. **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 5.1.4.3](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.1.4.3](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_3.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_3.ps1 index a24dbcb099ab..52e0eeba070b 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_3.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_3.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_5_1_4_3 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (5.1.4.3) - GA role SHALL NOT be added as local administrator during Entra join + Tests CIS M365 7.0.0 (5.1.4.3) - GA role SHALL NOT be added as local administrator during Entra join #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_4.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_4.md index ea353bf67422..6cd78adf6d24 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_4.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_4.md @@ -5,7 +5,7 @@ By default the user who registers a device gets local admin on that device. CIS Microsoft Entra admin centre > Devices > Device settings > Registering user is added as local administrator on the device during Microsoft Entra join: Selected or None. **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 5.1.4.4](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.1.4.4](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_4.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_4.ps1 index 1123427144a7..c654c536f441 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_4.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_4.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_5_1_4_4 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (5.1.4.4) - Local administrator assignment SHALL be limited during Entra join + Tests CIS M365 7.0.0 (5.1.4.4) - Local administrator assignment SHALL be limited during Entra join #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_5.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_5.md index 6d02eebd73f5..7dc51995ab64 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_5.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_5.md @@ -5,7 +5,7 @@ Without LAPS, the same local administrator password is often used on every devic Microsoft Entra admin centre > Devices > Device settings > Enable Microsoft Entra Local Administrator Password Solution: **Yes**. Then deploy an Intune Account Protection LAPS policy. **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 5.1.4.5](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.1.4.5](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_5.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_5.ps1 index 4f84abbede46..3f22ff0ffd7f 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_5.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_5.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_5_1_4_5 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (5.1.4.5) - Local Administrator Password Solution (LAPS) SHALL be enabled + Tests CIS M365 7.0.0 (5.1.4.5) - Local Administrator Password Solution (LAPS) SHALL be enabled #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_6.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_6.md index a5068db58168..bde98275794c 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_6.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_6.md @@ -7,7 +7,7 @@ Update-MgPolicyAuthorizationPolicy -DefaultUserRolePermissions @{ AllowedToReadB ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 5.1.4.6](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.1.4.6](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_6.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_6.ps1 index 251fe3a9fbe0..147aa44f9367 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_6.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_4_6.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_5_1_4_6 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (5.1.4.6) - Users SHALL be restricted from recovering BitLocker keys + Tests CIS M365 7.0.0 (5.1.4.6) - Users SHALL be restricted from recovering BitLocker keys #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_1.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_1.md index 71985d5f95ad..e6b1283c12ba 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_1.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_1.md @@ -9,7 +9,7 @@ Update-MgPolicyAuthorizationPolicy -DefaultUserRolePermissions @{ PermissionGran (Or assign `ManagePermissionGrantsForSelf.microsoft-user-default-low` if low-risk consent is acceptable.) **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 5.1.5.1](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.1.5.1](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_1.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_1.ps1 index 768ce0f1eb39..3b04daf9cee2 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_1.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_1.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_5_1_5_1 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (5.1.5.1) - User consent to apps accessing company data SHALL NOT be allowed + Tests CIS M365 7.0.0 (5.1.5.1) - User consent to apps accessing company data SHALL NOT be allowed #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_2.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_2.md index b9659376b5fe..40c95e5eadd5 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_2.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_2.md @@ -7,7 +7,7 @@ Update-MgPolicyAdminConsentRequestPolicy -IsEnabled $true -NotifyReviewers $true ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 5.1.5.2](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.1.5.2](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_2.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_2.ps1 index 263c028b1104..f933db5e1730 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_2.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_2.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_5_1_5_2 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (5.1.5.2) - The admin consent workflow SHALL be enabled + Tests CIS M365 7.0.0 (5.1.5.2) - The admin consent workflow SHALL be enabled #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_3.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_3.md new file mode 100644 index 000000000000..10a34d2960fc --- /dev/null +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_3.md @@ -0,0 +1,11 @@ +Client secrets (password credentials) are static strings with weaker security guarantees than certificates, and they are often stored in plaintext in source code, config files, and CI/CD pipelines. A leaked secret lets any holder authenticate as the application and reach whatever permissions it holds. Blocking new password credentials tenant-wide removes this attack surface for future applications and forces adoption of stronger credential types such as certificates. + +**Remediation Action** + +In Microsoft Entra admin center, go to Entra ID > Enterprise apps > Security > Application policies, open Block password addition, set Status to On, set Applies to to all applications (with reviewed exclusions if any), and save. + +**Links** +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.1.5.3](https://www.cisecurity.org/benchmark/microsoft_365) + + +%TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_3.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_3.ps1 new file mode 100644 index 000000000000..1e4b7dbbaf44 --- /dev/null +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_3.ps1 @@ -0,0 +1,37 @@ +function Invoke-CippTestCIS_5_1_5_3 { + <# + .SYNOPSIS + Tests CIS M365 7.0.0 (5.1.5.3) - Ensure password addition is blocked for applications + #> + param($Tenant) + + try { + $Policy = Get-CIPPTestData -TenantFilter $Tenant -Type 'DefaultAppManagementPolicy' + if (-not $Policy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_1_5_3' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'DefaultAppManagementPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'Medium' -Name 'Password addition is blocked for applications' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application Management' + return + } + $Cfg = $Policy | Select-Object -First 1 + + $Restriction = $Cfg.applicationRestrictions.passwordCredentials | Where-Object { $_.restrictionType -eq 'passwordAddition' } | Select-Object -First 1 + + if (-not $Cfg.isEnabled) { + $Status = 'Failed' + $Result = 'The default app management policy is not enabled (isEnabled is false). Password addition is not blocked for applications.' + } elseif (-not $Restriction) { + $Status = 'Failed' + $Result = 'No passwordAddition restriction is configured under the application restrictions. Password addition is not blocked.' + } elseif ($Restriction.state -ne 'enabled') { + $Status = 'Failed' + $Result = "The passwordAddition restriction is not enabled (state is '$($Restriction.state)'). Password addition is not blocked for applications." + } else { + $Status = 'Passed' + $Result = 'The default app management policy is enabled and the passwordAddition restriction is enabled. Password addition is blocked for applications.' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_1_5_3' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Password addition is blocked for applications' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application Management' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_1_5_3' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Password addition is blocked for applications' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application Management' + } +} diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_4.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_4.md new file mode 100644 index 000000000000..3d3ef654a98f --- /dev/null +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_4.md @@ -0,0 +1,11 @@ +Long-lived client secrets extend the window of exploitation when a credential is compromised. A secret that stays valid for years and is never rotated remains usable long after it leaks through source code, build logs, or a breach. Enforcing a maximum lifetime of 180 days or less ensures secrets expire regularly, limits how long a stolen credential is usable, and encourages automated rotation. + +**Remediation Action** + +In Microsoft Entra admin center, go to Entra ID > Enterprise apps > Security > Application policies, open Restrict max password lifetime, set Status to On, set the maximum lifetime to 180 days or less, set Applies to to all applications (with reviewed exclusions if any), and save. + +**Links** +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.1.5.4](https://www.cisecurity.org/benchmark/microsoft_365) + + +%TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_4.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_4.ps1 new file mode 100644 index 000000000000..2034110feb37 --- /dev/null +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_4.ps1 @@ -0,0 +1,46 @@ +function Invoke-CippTestCIS_5_1_5_4 { + <# + .SYNOPSIS + Tests CIS M365 7.0.0 (5.1.5.4) - Ensure password lifetime for applications does not exceed 180 days + #> + param($Tenant) + + try { + $Policy = Get-CIPPTestData -TenantFilter $Tenant -Type 'DefaultAppManagementPolicy' + if (-not $Policy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_1_5_4' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'DefaultAppManagementPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'Medium' -Name 'Password lifetime for applications does not exceed 180 days' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application Management' + return + } + $Cfg = $Policy | Select-Object -First 1 + + $Restriction = $Cfg.applicationRestrictions.passwordCredentials | Where-Object { $_.restrictionType -eq 'passwordLifetime' } | Select-Object -First 1 + + if (-not $Cfg.isEnabled) { + $Status = 'Failed' + $Result = 'The default app management policy is not enabled (isEnabled is false). A maximum password lifetime is not enforced for applications.' + } elseif (-not $Restriction) { + $Status = 'Failed' + $Result = 'No passwordLifetime restriction is configured under the application restrictions. A maximum password lifetime is not enforced.' + } elseif ($Restriction.state -ne 'enabled') { + $Status = 'Failed' + $Result = "The passwordLifetime restriction is not enabled (state is '$($Restriction.state)'). A maximum password lifetime is not enforced for applications." + } elseif ([string]::IsNullOrWhiteSpace($Restriction.maxLifetime)) { + $Status = 'Failed' + $Result = 'The passwordLifetime restriction is enabled but no maxLifetime value is set.' + } else { + $MaxDays = [System.Xml.XmlConvert]::ToTimeSpan($Restriction.maxLifetime).TotalDays + if ($MaxDays -le 180) { + $Status = 'Passed' + $Result = "The passwordLifetime restriction is enabled with a maximum lifetime of $($Restriction.maxLifetime) ($([int]$MaxDays) days), which does not exceed 180 days." + } else { + $Status = 'Failed' + $Result = "The passwordLifetime restriction is enabled but the maximum lifetime of $($Restriction.maxLifetime) ($([int]$MaxDays) days) exceeds 180 days." + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_1_5_4' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Password lifetime for applications does not exceed 180 days' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application Management' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_1_5_4' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Password lifetime for applications does not exceed 180 days' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application Management' + } +} diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_5.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_5.md new file mode 100644 index 000000000000..9befd0d98e56 --- /dev/null +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_5.md @@ -0,0 +1,11 @@ +Custom password values chosen by the caller are prone to low entropy, predictable patterns, and reuse across applications, making a compromised secret trivial to exploit. System-generated passwords use random values of sufficient length and complexity that resist brute-force and dictionary attacks. Blocking custom passwords removes the weakest credential creation path and ensures every new client secret meets a consistent entropy baseline. + +**Remediation Action** + +In Microsoft Entra admin center, go to Entra ID > Enterprise apps > Security > Application policies, open Block custom passwords, set Status to On, set Applies to to all applications (with reviewed exclusions if any), and save. + +**Links** +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.1.5.5](https://www.cisecurity.org/benchmark/microsoft_365) + + +%TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_5.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_5.ps1 new file mode 100644 index 000000000000..b5e13371edd2 --- /dev/null +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_5.ps1 @@ -0,0 +1,37 @@ +function Invoke-CippTestCIS_5_1_5_5 { + <# + .SYNOPSIS + Tests CIS M365 7.0.0 (5.1.5.5) - Ensure new application passwords are system-generated + #> + param($Tenant) + + try { + $Policy = Get-CIPPTestData -TenantFilter $Tenant -Type 'DefaultAppManagementPolicy' + if (-not $Policy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_1_5_5' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'DefaultAppManagementPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'Medium' -Name 'New application passwords are system-generated' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application Management' + return + } + $Cfg = $Policy | Select-Object -First 1 + + $Restriction = $Cfg.applicationRestrictions.passwordCredentials | Where-Object { $_.restrictionType -eq 'customPasswordAddition' } | Select-Object -First 1 + + if (-not $Cfg.isEnabled) { + $Status = 'Failed' + $Result = 'The default app management policy is not enabled (isEnabled is false). Custom application passwords are not blocked, so new passwords are not required to be system-generated.' + } elseif (-not $Restriction) { + $Status = 'Failed' + $Result = 'No customPasswordAddition restriction is configured under the application restrictions. Custom application passwords are not blocked.' + } elseif ($Restriction.state -ne 'enabled') { + $Status = 'Failed' + $Result = "The customPasswordAddition restriction is not enabled (state is '$($Restriction.state)'). Custom application passwords are not blocked, so new passwords are not required to be system-generated." + } else { + $Status = 'Passed' + $Result = 'The default app management policy is enabled and the customPasswordAddition restriction is enabled. Custom passwords are blocked, so new application passwords must be system-generated.' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_1_5_5' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'New application passwords are system-generated' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application Management' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_1_5_5' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'New application passwords are system-generated' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application Management' + } +} diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_6.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_6.md new file mode 100644 index 000000000000..9fda6eb04f02 --- /dev/null +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_6.md @@ -0,0 +1,11 @@ +Long-lived certificates extend the window of exploitation when a credential is compromised. A certificate valid for years that is never rotated stays usable long after its private key is exposed through a server breach, misconfigured storage, or a supply-chain compromise. Enforcing a maximum lifetime of 180 days or less ensures certificates expire regularly, limits how long a stolen credential is usable, and encourages automated certificate rotation. + +**Remediation Action** + +In Microsoft Entra admin center, go to Entra ID > Enterprise apps > Security > Application policies, open Restrict max certificate lifetime, set Status to On, set the maximum lifetime (in days) to 180 or less, set Applies to to all applications (with reviewed exclusions if any), and save. + +**Links** +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.1.5.6](https://www.cisecurity.org/benchmark/microsoft_365) + + +%TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_6.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_6.ps1 new file mode 100644 index 000000000000..80aded2569c8 --- /dev/null +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_5_6.ps1 @@ -0,0 +1,46 @@ +function Invoke-CippTestCIS_5_1_5_6 { + <# + .SYNOPSIS + Tests CIS M365 7.0.0 (5.1.5.6) - Ensure maximum certificate lifetime for applications does not exceed 180 days + #> + param($Tenant) + + try { + $Policy = Get-CIPPTestData -TenantFilter $Tenant -Type 'DefaultAppManagementPolicy' + if (-not $Policy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_1_5_6' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'DefaultAppManagementPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'Medium' -Name 'Maximum certificate lifetime for applications does not exceed 180 days' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application Management' + return + } + $Cfg = $Policy | Select-Object -First 1 + + $Restriction = $Cfg.applicationRestrictions.keyCredentials | Where-Object { $_.restrictionType -eq 'asymmetricKeyLifetime' } | Select-Object -First 1 + + if (-not $Cfg.isEnabled) { + $Status = 'Failed' + $Result = 'The default app management policy is not enabled (isEnabled is false). A maximum certificate lifetime is not enforced for applications.' + } elseif (-not $Restriction) { + $Status = 'Failed' + $Result = 'No asymmetricKeyLifetime restriction is configured under the application key credential restrictions. A maximum certificate lifetime is not enforced.' + } elseif ($Restriction.state -ne 'enabled') { + $Status = 'Failed' + $Result = "The asymmetricKeyLifetime restriction is not enabled (state is '$($Restriction.state)'). A maximum certificate lifetime is not enforced for applications." + } elseif ([string]::IsNullOrWhiteSpace($Restriction.maxLifetime)) { + $Status = 'Failed' + $Result = 'The asymmetricKeyLifetime restriction is enabled but no maxLifetime value is set.' + } else { + $MaxDays = [System.Xml.XmlConvert]::ToTimeSpan($Restriction.maxLifetime).TotalDays + if ($MaxDays -le 180) { + $Status = 'Passed' + $Result = "The asymmetricKeyLifetime restriction is enabled with a maximum lifetime of $($Restriction.maxLifetime) ($([int]$MaxDays) days), which does not exceed 180 days." + } else { + $Status = 'Failed' + $Result = "The asymmetricKeyLifetime restriction is enabled but the maximum lifetime of $($Restriction.maxLifetime) ($([int]$MaxDays) days) exceeds 180 days." + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_1_5_6' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Maximum certificate lifetime for applications does not exceed 180 days' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application Management' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_1_5_6' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Maximum certificate lifetime for applications does not exceed 180 days' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application Management' + } +} diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_6_1.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_6_1.md index 7990d1607f8e..2d79ad499cec 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_6_1.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_6_1.md @@ -5,7 +5,7 @@ An open guest invitation policy lets users invite anyone with any email — a fr Configure `invitationsAllowedAndBlockedDomainsPolicy` on the cross-tenant access / B2B settings, with an explicit `allowedDomains` allowlist (or `blockedDomains` denylist). **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 5.1.6.1](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.1.6.1](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_6_1.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_6_1.ps1 index 37e36fd9b18b..c2b5a1b5d0f7 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_6_1.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_6_1.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_5_1_6_1 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (5.1.6.1) - Collaboration invitations SHALL be sent to allowed domains only + Tests CIS M365 7.0.0 (5.1.6.1) - Collaboration invitations SHALL be sent to allowed domains only #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_6_2.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_6_2.md index f1e29f387531..707ef85ec690 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_6_2.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_6_2.md @@ -7,7 +7,7 @@ Update-MgPolicyAuthorizationPolicy -GuestUserRoleId '2af84b1e-32c8-42b7-82bc-daa ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 5.1.6.2](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.1.6.2](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_6_2.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_6_2.ps1 index ccee7f0f1c3a..58c301eae9c9 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_6_2.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_6_2.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_5_1_6_2 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (5.1.6.2) - Guest user access SHALL be restricted + Tests CIS M365 7.0.0 (5.1.6.2) - Guest user access SHALL be restricted #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_6_3.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_6_3.md index 7a01851e0494..6dfbcba0a435 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_6_3.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_6_3.md @@ -7,7 +7,7 @@ Update-MgPolicyAuthorizationPolicy -AllowInvitesFrom 'adminsAndGuestInviters' ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 5.1.6.3](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.1.6.3](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_6_3.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_6_3.ps1 index a2e61c4b27cc..8d05c1c923e0 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_6_3.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_6_3.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_5_1_6_3 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (5.1.6.3) - Guest user invitations SHALL be limited to the Guest Inviter role + Tests CIS M365 7.0.0 (5.1.6.3) - Guest user invitations SHALL be limited to the Guest Inviter role #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_8_1.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_8_1.md index e8c95cb230a6..fab2d87bbc46 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_8_1.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_8_1.md @@ -5,7 +5,7 @@ PHS enables Microsoft to detect leaked credentials, supports break-glass when on Entra Connect Sync > Configure > Customize Synchronization Options > Optional Features > Password Hash Synchronization. **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 5.1.8.1](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.1.8.1](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_8_1.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_8_1.ps1 index 34b0426f7dc5..da73397cb263 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_8_1.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_1_8_1.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_5_1_8_1 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (5.1.8.1) - Password hash sync SHALL be enabled for hybrid deployments (Manual) + Tests CIS M365 7.0.0 (5.1.8.1) - Password hash sync SHALL be enabled for hybrid deployments (Manual) #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_1.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_1.md index d5582a961d01..fab2c8600d3a 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_1.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_1.md @@ -5,7 +5,7 @@ Privileged accounts are the highest-value target. MFA is the single most effecti Create a Conditional Access policy that targets all privileged role IDs and requires MFA (or, preferably, phishing-resistant MFA — see 5.2.2.5). **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 5.2.2.1](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.2.2.1](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_1.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_1.ps1 index 730b29f55a04..3cd5c1eca8f6 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_1.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_1.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_5_2_2_1 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (5.2.2.1) - MFA SHALL be enabled for all users in administrative roles + Tests CIS M365 7.0.0 (5.2.2.1) - MFA SHALL be enabled for all users in administrative roles #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_10.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_10.md index 59303766e2a7..9005a4a52725 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_10.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_10.md @@ -7,7 +7,7 @@ Conditional Access policy: - Grant: Require compliant device (or limit by trusted locations) **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 5.2.2.10](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.2.2.10](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_10.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_10.ps1 index cb35274f04eb..611801462fb5 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_10.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_10.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_5_2_2_10 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (5.2.2.10) - A managed device SHALL be required to register security information + Tests CIS M365 7.0.0 (5.2.2.10) - A managed device SHALL be required to register security information #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_11.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_11.md index a784e10b0c0f..f26bca4adf27 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_11.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_11.md @@ -7,7 +7,7 @@ Conditional Access policy: - Session > Sign-in frequency: Every time **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 5.2.2.11](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.2.2.11](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_11.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_11.ps1 index a96f53f0eb90..7fbb26dcef4c 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_11.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_11.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_5_2_2_11 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (5.2.2.11) - Sign-in frequency for Intune Enrollment SHALL be 'Every time' + Tests CIS M365 7.0.0 (5.2.2.11) - Sign-in frequency for Intune Enrollment SHALL be 'Every time' #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_12.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_12.md index 2043faf699db..0b94494a3600 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_12.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_12.md @@ -8,7 +8,7 @@ Conditional Access policy: - Grant: Block **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 5.2.2.12](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.2.2.12](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_12.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_12.ps1 index a12b73f3b461..97a7646c237f 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_12.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_12.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_5_2_2_12 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (5.2.2.12) - The device code sign-in flow SHALL be blocked + Tests CIS M365 7.0.0 (5.2.2.12) - The device code sign-in flow SHALL be blocked #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_13.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_13.md new file mode 100644 index 000000000000..6cec22869e5b --- /dev/null +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_13.md @@ -0,0 +1,15 @@ +Sign-in frequency defines how long before a user must reauthenticate to access a resource. The Entra ID default is a rolling 90-day window, which extends the lifespan of stolen tokens or compromised credentials. Enforcing periodic reauthentication of 7 days or less for all users limits that exposure while keeping reauthentication prompts infrequent enough to avoid user fatigue. + +**Remediation Action** + +Conditional Access policy: +- Users: All users (exclude break-glass accounts) +- Target resources: All resources (All cloud apps) +- Session > Sign-in frequency: Periodic reauthentication set to 7 days or less +- Enable policy: On + +**Links** +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.2.2.13](https://www.cisecurity.org/benchmark/microsoft_365) + + +%TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_13.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_13.ps1 new file mode 100644 index 000000000000..7e12616d8462 --- /dev/null +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_13.ps1 @@ -0,0 +1,43 @@ +function Invoke-CippTestCIS_5_2_2_13 { + <# + .SYNOPSIS + Tests CIS M365 7.0.0 (5.2.2.13) - Periodic reauthentication is required for all users + #> + param($Tenant) + + try { + $CA = Get-CIPPTestData -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' + + if (-not $CA) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_2_2_13' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'ConditionalAccessPolicies cache not found.' -Risk 'High' -Name 'Periodic reauthentication is required for all users' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Session Management' + return + } + + $Matching = $CA | Where-Object { + $_.state -eq 'enabled' -and + $_.conditions.users.includeUsers -contains 'All' -and + $_.conditions.applications.includeApplications -contains 'All' -and + $_.sessionControls.signInFrequency -and + $_.sessionControls.signInFrequency.isEnabled -eq $true -and + $_.sessionControls.signInFrequency.frequencyInterval -eq 'timeBased' -and + ( + ($_.sessionControls.signInFrequency.type -eq 'days' -and $_.sessionControls.signInFrequency.value -le 7) -or + ($_.sessionControls.signInFrequency.type -eq 'hours' -and $_.sessionControls.signInFrequency.value -le 168) + ) + } + + if ($Matching) { + $Status = 'Passed' + $Result = "$($Matching.Count) Conditional Access policy/policies enforce periodic reauthentication (7 days or less) for all users:`n`n" + $Result += ($Matching | ForEach-Object { "- $($_.displayName) ($($_.sessionControls.signInFrequency.value) $($_.sessionControls.signInFrequency.type))" }) -join "`n" + } else { + $Status = 'Failed' + $Result = 'No enabled Conditional Access policy targets all users with a periodic (timeBased) sign-in frequency of 7 days or less.' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_2_2_13' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Periodic reauthentication is required for all users' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Session Management' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_2_2_13' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Periodic reauthentication is required for all users' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Session Management' + } +} diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_14.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_14.md new file mode 100644 index 000000000000..3d4d4b8a6b22 --- /dev/null +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_14.md @@ -0,0 +1,12 @@ +Entra ID Conditional Access lets an organization define named locations - either geographic locations or specific IP addresses and ranges - and mark IP-based locations as trusted. Defining and applying named locations lets policies tailor access requirements based on whether a sign-in originates from a trusted or untrusted network, and improves the accuracy of Entra ID Protection risk evaluations. The recommended state is to define at least one named location and reference it in a Conditional Access policy. + +**Remediation Action** + +1. Define a named location under Entra ID > Conditional Access > Named locations (for IP ranges, mark as trusted). +2. Reference that named location in the location conditions of at least one enabled Conditional Access policy. + +**Links** +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.2.2.14](https://www.cisecurity.org/benchmark/microsoft_365) + + +%TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_14.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_14.ps1 new file mode 100644 index 000000000000..0c76e031088a --- /dev/null +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_14.ps1 @@ -0,0 +1,40 @@ +function Invoke-CippTestCIS_5_2_2_14 { + <# + .SYNOPSIS + Tests CIS M365 7.0.0 (5.2.2.14) - Named locations are defined and applied + #> + param($Tenant) + + try { + $CA = Get-CIPPTestData -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' + + if (-not $CA) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_2_2_14' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'ConditionalAccessPolicies cache not found.' -Risk 'High' -Name 'Named locations are defined and applied' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication' + return + } + + # A named location reference is any include/exclude location GUID (i.e. not 'All' or 'AllTrusted') + $Matching = $CA | Where-Object { + $_.state -eq 'enabled' -and + $_.conditions.locations -and + ( + ($_.conditions.locations.includeLocations | Where-Object { $_ -and $_ -notin @('All', 'AllTrusted') }) -or + ($_.conditions.locations.excludeLocations | Where-Object { $_ -and $_ -notin @('All', 'AllTrusted') }) + ) + } + + if ($Matching) { + $Status = 'Passed' + $Result = "$($Matching.Count) enabled Conditional Access policy/policies reference a named location:`n`n" + $Result += ($Matching | ForEach-Object { "- $($_.displayName)" }) -join "`n" + } else { + $Status = 'Failed' + $Result = 'No enabled Conditional Access policy references a defined named location in its location conditions.' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_2_2_14' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Named locations are defined and applied' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_2_2_14' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Named locations are defined and applied' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication' + } +} diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_15.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_15.md new file mode 100644 index 000000000000..d1656ed675d7 --- /dev/null +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_15.md @@ -0,0 +1,17 @@ +Conditional Access policies can block access from geographic locations that are out-of-scope for the organization or application. Using Conditional Access as a deny list lets an organization block traffic from regions outside its operational scope or legal jurisdiction, significantly reducing exposure to international threat actors and advanced persistent threats. The recommended state is at least one policy that blocks access from untrusted locations while excluding the trusted locations that should remain allowed. + +**Remediation Action** + +Conditional Access policy: +- Users: All users (exclude break-glass accounts) +- Target resources: All resources (All cloud apps) +- Network > Include: the untrusted locations to block +- Network > Exclude: All trusted locations (or the trusted locations to allow) +- Grant: Block access +- Enable policy: On + +**Links** +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.2.2.15](https://www.cisecurity.org/benchmark/microsoft_365) + + +%TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_15.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_15.ps1 new file mode 100644 index 000000000000..f94100369803 --- /dev/null +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_15.ps1 @@ -0,0 +1,45 @@ +function Invoke-CippTestCIS_5_2_2_15 { + <# + .SYNOPSIS + Tests CIS M365 7.0.0 (5.2.2.15) - Exclusionary geographic access controls are utilized + #> + param($Tenant) + + try { + $CA = Get-CIPPTestData -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' + + if (-not $CA) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_2_2_15' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'ConditionalAccessPolicies cache not found.' -Risk 'High' -Name 'Exclusionary geographic access controls are utilized' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Authentication' + return + } + + $Matching = $CA | Where-Object { + $_.state -eq 'enabled' -and + $_.conditions.users.includeUsers -contains 'All' -and + $_.conditions.applications.includeApplications -contains 'All' -and + $_.grantControls.builtInControls -contains 'block' -and + $_.conditions.locations -and + # Include at least one untrusted/selected location (a GUID, not just 'AllTrusted') + ($_.conditions.locations.includeLocations | Where-Object { $_ -and $_ -ne 'AllTrusted' }) -and + # Exclude trusted locations: AllTrusted, or at least one location GUID + ( + $_.conditions.locations.excludeLocations -contains 'AllTrusted' -or + ($_.conditions.locations.excludeLocations | Where-Object { $_ }) + ) + } + + if ($Matching) { + $Status = 'Passed' + $Result = "$($Matching.Count) enabled Conditional Access policy/policies block access from untrusted locations while excluding trusted locations:`n`n" + $Result += ($Matching | ForEach-Object { "- $($_.displayName)" }) -join "`n" + } else { + $Status = 'Failed' + $Result = 'No enabled block policy was found that includes untrusted locations for all users/apps and excludes trusted locations.' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_2_2_15' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Exclusionary geographic access controls are utilized' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Authentication' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_2_2_15' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Exclusionary geographic access controls are utilized' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Authentication' + } +} diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_16.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_16.md new file mode 100644 index 000000000000..c16bd1d4d55e --- /dev/null +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_16.md @@ -0,0 +1,17 @@ +Token Protection is a Conditional Access session control that reduces token replay attacks by ensuring only device-bound sign-in session tokens, such as Primary Refresh Tokens, are accepted when applications request access to protected resources. Because the token is cryptographically bound to a registered device, a stolen token cannot be replayed from another device. The recommended state is to enforce Token Protection for Office 365 Exchange Online, SharePoint Online and Microsoft Teams Services. + +**Remediation Action** + +Conditional Access policy: +- Users: selected users/groups (exclude break-glass accounts) +- Target resources: Office 365 Exchange Online, SharePoint Online, Microsoft Teams Services +- Conditions > Device platforms: Windows +- Conditions > Client apps: Mobile apps and desktop clients +- Session: Require token protection for sign-in sessions +- Enable policy: On + +**Links** +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.2.2.16](https://www.cisecurity.org/benchmark/microsoft_365) + + +%TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_16.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_16.ps1 new file mode 100644 index 000000000000..6e8d90713356 --- /dev/null +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_16.ps1 @@ -0,0 +1,47 @@ +function Invoke-CippTestCIS_5_2_2_16 { + <# + .SYNOPSIS + Tests CIS M365 7.0.0 (5.2.2.16) - Token Protection is enforced for session tokens + #> + param($Tenant) + + try { + $CA = Get-CIPPTestData -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' + + if (-not $CA) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_2_2_16' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'ConditionalAccessPolicies cache not found.' -Risk 'High' -Name 'Token Protection is enforced for session tokens' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Session Management' + return + } + + # Office 365 Exchange Online, SharePoint Online and Microsoft Teams Services app GUIDs + $ExchangeOnline = '00000002-0000-0ff1-ce00-000000000000' + $SharePointOnline = '00000003-0000-0ff1-ce00-000000000000' + $TeamsServices = 'cc15fd57-2c6c-4117-a88c-83b1d56b4bbe' + + $Matching = $CA | Where-Object { + $_.state -eq 'enabled' -and + $_.conditions.users.includeUsers -notcontains 'None' -and + $_.conditions.applications.includeApplications -contains $ExchangeOnline -and + $_.conditions.applications.includeApplications -contains $SharePointOnline -and + $_.conditions.applications.includeApplications -contains $TeamsServices -and + $_.conditions.platforms.includePlatforms -contains 'windows' -and + $_.conditions.clientAppTypes -contains 'mobileAppsAndDesktopClients' -and + $_.sessionControls.secureSignInSession -and + $_.sessionControls.secureSignInSession.isEnabled -eq $true + } + + if ($Matching) { + $Status = 'Passed' + $Result = "$($Matching.Count) Conditional Access policy/policies enforce Token Protection for sign-in sessions:`n`n" + $Result += ($Matching | ForEach-Object { "- $($_.displayName)" }) -join "`n" + } else { + $Status = 'Failed' + $Result = 'No enabled Conditional Access policy enforces Token Protection (secure sign-in session) for Exchange Online, SharePoint Online and Teams on Windows desktop/mobile clients.' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_2_2_16' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Token Protection is enforced for session tokens' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Session Management' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_2_2_16' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Token Protection is enforced for session tokens' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Session Management' + } +} diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_17.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_17.md new file mode 100644 index 000000000000..1d578586a2b9 --- /dev/null +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_17.md @@ -0,0 +1,16 @@ +Authentication transfer is a flow that lets users seamlessly transfer authenticated state from one device to another, such as scanning a QR code in Outlook desktop to sign into Outlook mobile. Blocking it protects against token theft and replay by preventing device tokens from silently authenticating on other devices or browsers, ensuring each authentication request originates from the original device and remains subject to device compliance and session checks. The recommended state is to block authentication transfer. + +**Remediation Action** + +Conditional Access policy: +- Users: All users (exclude break-glass accounts) +- Target resources: All resources (All cloud apps) +- Conditions > Authentication flows: Authentication transfer +- Grant: Block access +- Enable policy: On + +**Links** +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.2.2.17](https://www.cisecurity.org/benchmark/microsoft_365) + + +%TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_17.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_17.ps1 new file mode 100644 index 000000000000..77197773c95c --- /dev/null +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_17.ps1 @@ -0,0 +1,39 @@ +function Invoke-CippTestCIS_5_2_2_17 { + <# + .SYNOPSIS + Tests CIS M365 7.0.0 (5.2.2.17) - Authentication transfer is blocked + #> + param($Tenant) + + try { + $CA = Get-CIPPTestData -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' + + if (-not $CA) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_2_2_17' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'ConditionalAccessPolicies cache not found.' -Risk 'High' -Name 'Authentication transfer is blocked' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication' + return + } + + $Matching = $CA | Where-Object { + $_.state -eq 'enabled' -and + $_.conditions.users.includeUsers -contains 'All' -and + $_.conditions.applications.includeApplications -contains 'All' -and + $_.conditions.authenticationFlows -and + $_.conditions.authenticationFlows.transferMethods -match 'authenticationTransfer' -and + $_.grantControls.builtInControls -contains 'block' + } + + if ($Matching) { + $Status = 'Passed' + $Result = "$($Matching.Count) Conditional Access policy/policies block authentication transfer:`n`n" + $Result += ($Matching | ForEach-Object { "- $($_.displayName)" }) -join "`n" + } else { + $Status = 'Failed' + $Result = 'No enabled Conditional Access policy blocks the authenticationTransfer authentication flow for all users.' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_2_2_17' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Authentication transfer is blocked' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_2_2_17' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Authentication transfer is blocked' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication' + } +} diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_2.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_2.md index 553a49f1e350..dfe03425f470 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_2.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_2.md @@ -8,7 +8,7 @@ Create a Conditional Access policy: - Grant: Require MFA **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 5.2.2.2](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.2.2.2](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_2.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_2.ps1 index 5504a6bc1a09..e68f05775a62 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_2.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_2.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_5_2_2_2 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (5.2.2.2) - MFA SHALL be enabled for all users + Tests CIS M365 7.0.0 (5.2.2.2) - MFA SHALL be enabled for all users #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_3.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_3.md index 80a74bf6f2ee..59f27df274c3 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_3.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_3.md @@ -9,7 +9,7 @@ Conditional Access policy with: - Grant: Block **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 5.2.2.3](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.2.2.3](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_3.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_3.ps1 index 0f1f431783c4..9da168bf96b3 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_3.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_3.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_5_2_2_3 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (5.2.2.3) - Conditional Access policies SHALL block legacy authentication + Tests CIS M365 7.0.0 (5.2.2.3) - Conditional Access policies SHALL block legacy authentication #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_4.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_4.md index 4b8dc5968e78..0b0ba665633f 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_4.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_4.md @@ -7,7 +7,7 @@ Conditional Access policy targeting privileged roles with: - Session > Persistent browser: Never persistent **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 5.2.2.4](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.2.2.4](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_4.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_4.ps1 index ea2462c3b457..c30b77edc38a 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_4.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_4.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_5_2_2_4 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (5.2.2.4) - Sign-in frequency SHALL be enabled and browser sessions not persistent for administrative users + Tests CIS M365 7.0.0 (5.2.2.4) - Sign-in frequency SHALL be enabled and browser sessions not persistent for administrative users #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_5.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_5.md index a9d9cfcc4014..bd46bbb8860f 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_5.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_5.md @@ -5,7 +5,7 @@ Number-matched push, SMS and voice MFA can all be defeated by AiTM phishing kits Conditional Access policy targeting privileged roles, with grant control = authentication strength = "Phishing-resistant MFA". **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 5.2.2.5](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.2.2.5](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_5.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_5.ps1 index 0b8e1b288ce7..bfc12f0bceed 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_5.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_5.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_5_2_2_5 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (5.2.2.5) - 'Phishing-resistant MFA strength' SHALL be required for Administrators + Tests CIS M365 7.0.0 (5.2.2.5) - 'Phishing-resistant MFA strength' SHALL be required for Administrators #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_6.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_6.md index 48535387cdf3..6ef32feb3774 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_6.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_6.md @@ -8,7 +8,7 @@ Conditional Access policy: - Grant: Require password change + MFA **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 5.2.2.6](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.2.2.6](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_6.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_6.ps1 index efbd1b399175..ec3e75261b14 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_6.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_6.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_5_2_2_6 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (5.2.2.6) - Identity Protection user risk policies SHALL be enabled + Tests CIS M365 7.0.0 (5.2.2.6) - Identity Protection user risk policies SHALL be enabled #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_7.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_7.md index ab1605470019..075717782a1a 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_7.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_7.md @@ -8,7 +8,7 @@ Conditional Access policy: - Grant: Require MFA **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 5.2.2.7](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.2.2.7](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_7.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_7.ps1 index 985192b7ae2a..a11876a8bc5c 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_7.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_7.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_5_2_2_7 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (5.2.2.7) - Identity Protection sign-in risk policies SHALL be enabled + Tests CIS M365 7.0.0 (5.2.2.7) - Identity Protection sign-in risk policies SHALL be enabled #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_8.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_8.md index 057d51a8a1b4..ecb089706512 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_8.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_8.md @@ -8,7 +8,7 @@ Conditional Access policy: - Grant: Block **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 5.2.2.8](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.2.2.8](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_8.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_8.ps1 index ebd0894284e2..2b85e851b991 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_8.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_8.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_5_2_2_8 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (5.2.2.8) - 'sign-in risk' SHALL be blocked for medium and high risk + Tests CIS M365 7.0.0 (5.2.2.8) - 'sign-in risk' SHALL be blocked for medium and high risk #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_9.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_9.md index afa4a7c89382..846d40d4be59 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_9.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_9.md @@ -5,7 +5,7 @@ Requiring a compliant or hybrid-joined device for authentication tightly couples Conditional Access policy with grant: Require device to be marked as compliant OR hybrid Microsoft Entra joined. **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 5.2.2.9](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.2.2.9](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_9.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_9.ps1 index 2610d62f8d68..e0db00340271 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_9.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_2_9.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_5_2_2_9 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (5.2.2.9) - A managed device SHALL be required for authentication + Tests CIS M365 7.0.0 (5.2.2.9) - A managed device SHALL be required for authentication #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_1.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_1.md index 6125324d5e37..83c6b1e50307 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_1.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_1.md @@ -5,7 +5,7 @@ MFA fatigue (push-bombing) succeeds when users approve a request without context Microsoft Entra > Authentication methods > Microsoft Authenticator > Configure: enable "Show application name in push and passwordless notifications" and "Show geographic location in push and passwordless notifications" for All users. **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 5.2.3.1](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.2.3.1](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_1.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_1.ps1 index 80d44fe1f053..dc659d397f42 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_1.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_1.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_5_2_3_1 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (5.2.3.1) - Microsoft Authenticator SHALL be configured to protect against MFA fatigue + Tests CIS M365 7.0.0 (5.2.3.1) - Microsoft Authenticator SHALL be configured to protect against MFA fatigue #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_10.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_10.md new file mode 100644 index 000000000000..1d5f77f3782e --- /dev/null +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_10.md @@ -0,0 +1,11 @@ +Authenticator Lite embeds a subset of Microsoft Authenticator into companion apps such as Outlook mobile, letting users satisfy MFA without the standalone app. However, it does not show application name or geographic location context in push notifications, does not satisfy Conditional Access authentication strength requirements, and does not support passwordless sign-in or SSPR via push. Disabling Microsoft Authenticator on companion applications ensures users authenticate through the full app where all MFA fatigue defenses are active. + +**Remediation Action** + +Microsoft Entra > Authentication methods > Policies > Microsoft Authenticator > Configure: set Microsoft Authenticator on companion applications Status to Disabled, then save. + +**Links** +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.2.3.10](https://www.cisecurity.org/benchmark/microsoft_365) + + +%TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_10.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_10.ps1 new file mode 100644 index 000000000000..ef8bc6009c22 --- /dev/null +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_10.ps1 @@ -0,0 +1,42 @@ +function Invoke-CippTestCIS_5_2_3_10 { + <# + .SYNOPSIS + Tests CIS M365 7.0.0 (5.2.3.10) - Ensure Microsoft Authenticator on companion applications is disabled + #> + param($Tenant) + + try { + $AMP = Get-CIPPTestData -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' + + if (-not $AMP) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_2_3_10' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'AuthenticationMethodsPolicy cache not found.' -Risk 'Medium' -Name 'Microsoft Authenticator on companion applications is disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication' + return + } + + $Cfg = $AMP | Select-Object -First 1 + $Authenticator = $Cfg.authenticationMethodConfigurations | Where-Object { $_.id -eq 'MicrosoftAuthenticator' } | Select-Object -First 1 + + if (-not $Authenticator) { + $Status = 'Failed' + $Result = 'MicrosoftAuthenticator authentication method configuration not found.' + } else { + $CompanionState = $Authenticator.featureSettings.companionAppAllowedState.state + + if ($Authenticator.state -eq 'disabled') { + $Status = 'Passed' + $Result = 'Microsoft Authenticator is disabled, so companion applications (Authenticator Lite) cannot be used.' + } elseif ($CompanionState -eq 'disabled') { + $Status = 'Passed' + $Result = 'Microsoft Authenticator on companion applications is disabled.' + } else { + $Status = 'Failed' + $Result = "Microsoft Authenticator on companion applications is not disabled. Current companionAppAllowedState: $CompanionState" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_2_3_10' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Microsoft Authenticator on companion applications is disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_2_3_10' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Microsoft Authenticator on companion applications is disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication' + } +} diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_2.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_2.md index 197f6bb1ea99..3b10225189aa 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_2.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_2.md @@ -5,7 +5,7 @@ Microsoft's global banned password list catches obvious bad passwords. The custo Microsoft Entra > Protection > Authentication methods > Password protection: Enforce custom list = Yes, Custom banned password list = (organisation-specific words). **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 5.2.3.2](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.2.3.2](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_2.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_2.ps1 index f0e0032f552e..2b204dab1ef3 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_2.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_2.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_5_2_3_2 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (5.2.3.2) - Custom banned passwords lists SHALL be used + Tests CIS M365 7.0.0 (5.2.3.2) - Custom banned passwords lists SHALL be used #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_3.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_3.md index b3eab6fbee1a..f3524e71e85b 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_3.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_3.md @@ -6,7 +6,7 @@ Without on-prem password protection, weak passwords created in AD never reach Mi 2. Microsoft Entra > Authentication methods > Password protection: Enable password protection on Windows Server Active Directory + Mode = Enforced. **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 5.2.3.3](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.2.3.3](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_3.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_3.ps1 index 3931d96602ee..cf731fd41c4e 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_3.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_3.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_5_2_3_3 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (5.2.3.3) - Password protection SHALL be enabled for on-prem Active Directory + Tests CIS M365 7.0.0 (5.2.3.3) - Password protection SHALL be enabled for on-prem Active Directory #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_4.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_4.md index 7e95c03feb6a..2a2db8d89af5 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_4.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_4.md @@ -6,7 +6,7 @@ A user is "MFA capable" once they have at least one strong authentication method 2. Run the User Registration Details report (`/reports/authenticationMethods/userRegistrationDetails`) and chase any user with `isMfaCapable = false`. **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 5.2.3.4](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.2.3.4](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_4.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_4.ps1 index d8d2e8b8df21..83ea94413d2e 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_4.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_4.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_5_2_3_4 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (5.2.3.4) - All member users SHALL be 'MFA capable' + Tests CIS M365 7.0.0 (5.2.3.4) - All member users SHALL be 'MFA capable' #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_5.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_5.md index 5557ca4e731b..1a617653826a 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_5.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_5.md @@ -5,7 +5,7 @@ SMS and Voice MFA are vulnerable to SIM swapping, telco intercepts, and call for Microsoft Entra > Authentication methods > Policies — disable SMS and Voice call. **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 5.2.3.5](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.2.3.5](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_5.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_5.ps1 index 5e031ae70fe8..49fb439dfabd 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_5.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_5.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_5_2_3_5 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (5.2.3.5) - Weak authentication methods SHALL be disabled (SMS, Voice) + Tests CIS M365 7.0.0 (5.2.3.5) - Weak authentication methods SHALL be disabled (SMS, Voice) #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_6.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_6.md index e1bdd80df37e..b148b9e1a9d0 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_6.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_6.md @@ -5,7 +5,7 @@ System-preferred MFA always offers the user their strongest registered method, d Microsoft Entra > Authentication methods > Settings: System-preferred multifactor authentication = Enabled, Include = All users. **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 5.2.3.6](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.2.3.6](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_6.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_6.ps1 index 877445736a51..e2f51c2a5f83 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_6.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_6.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_5_2_3_6 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (5.2.3.6) - System-preferred multifactor authentication SHALL be enabled + Tests CIS M365 7.0.0 (5.2.3.6) - System-preferred multifactor authentication SHALL be enabled #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_7.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_7.md index 594fb9c347df..86f94955e0ed 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_7.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_7.md @@ -5,7 +5,7 @@ Email OTP delivers MFA codes to a guest's mailbox — if that mailbox is comprom Microsoft Entra > Authentication methods > Email OTP — set to Disabled. **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 5.2.3.7](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.2.3.7](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_7.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_7.ps1 index c5fa41c270bf..381400b19d64 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_7.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_7.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_5_2_3_7 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (5.2.3.7) - The email OTP authentication method SHALL be disabled + Tests CIS M365 7.0.0 (5.2.3.7) - The email OTP authentication method SHALL be disabled #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_8.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_8.md new file mode 100644 index 000000000000..de941fc64385 --- /dev/null +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_8.md @@ -0,0 +1,11 @@ +Account lockout protects against brute-force and password spray attacks by locking an account once a set number of failed sign-ins is reached. The lockout threshold defines how many failed attempts are permitted before the account enters a locked-out state. A threshold of 10 or less limits how many password guesses an attacker can make in a given period while avoiding excessive lockouts for legitimate users. + +**Remediation Action** + +Microsoft Entra > Authentication methods > Password protection: set Lockout threshold to 10 or less. + +**Links** +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.2.3.8](https://www.cisecurity.org/benchmark/microsoft_365) + + +%TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_8.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_8.ps1 new file mode 100644 index 000000000000..1aa34116fbd4 --- /dev/null +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_8.ps1 @@ -0,0 +1,39 @@ +function Invoke-CippTestCIS_5_2_3_8 { + <# + .SYNOPSIS + Tests CIS M365 7.0.0 (5.2.3.8) - Ensure that Account 'Lockout threshold' is '10' or less + #> + param($Tenant) + + try { + $Settings = Get-CIPPTestData -TenantFilter $Tenant -Type 'Settings' + + if (-not $Settings) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_2_3_8' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'Settings cache not found.' -Risk 'Medium' -Name "Account 'Lockout threshold' is '10' or less" -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication' + return + } + + $PwdSetting = $Settings | Where-Object { $_.templateId -eq '5cf42378-d67d-4f36-ba46-e8b86229381d' -or $_.displayName -eq 'Password Rule Settings' } | Select-Object -First 1 + + if (-not $PwdSetting) { + # Password Rule Settings object not present: tenant defaults apply (LockoutThreshold default = 10), which satisfies the recommendation. + $Status = 'Passed' + $Result = "Password Rule Settings not configured; the tenant default Lockout threshold of 10 applies, which is compliant (10 or less)." + } else { + $LockoutThreshold = ($PwdSetting.values | Where-Object { $_.name -eq 'LockoutThreshold' }).value + + if ([int]$LockoutThreshold -le 10) { + $Status = 'Passed' + $Result = "Account Lockout threshold is set to $([int]$LockoutThreshold), which is 10 or less." + } else { + $Status = 'Failed' + $Result = "Account Lockout threshold is set to $([int]$LockoutThreshold), which exceeds the maximum recommended value of 10." + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_2_3_8' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name "Account 'Lockout threshold' is '10' or less" -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_2_3_8' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name "Account 'Lockout threshold' is '10' or less" -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication' + } +} diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_9.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_9.md new file mode 100644 index 000000000000..64258c87c963 --- /dev/null +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_9.md @@ -0,0 +1,11 @@ +Account lockout duration determines how long an account stays locked out after exceeding the lockout threshold, and therefore how long an attacker must wait before resuming attempts. A duration of at least 60 seconds, combined with a reasonable threshold, reduces the total number of failed sign-in attempts a malicious actor can perform over time while limiting inconvenience to legitimate users. + +**Remediation Action** + +Microsoft Entra > Authentication methods > Password protection: set Lockout duration in seconds to 60 or higher, then save. + +**Links** +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.2.3.9](https://www.cisecurity.org/benchmark/microsoft_365) + + +%TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_9.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_9.ps1 new file mode 100644 index 000000000000..c9c0705e8bc6 --- /dev/null +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_3_9.ps1 @@ -0,0 +1,39 @@ +function Invoke-CippTestCIS_5_2_3_9 { + <# + .SYNOPSIS + Tests CIS M365 7.0.0 (5.2.3.9) - Ensure that Account 'Lockout duration in seconds' is at least 60 seconds + #> + param($Tenant) + + try { + $Settings = Get-CIPPTestData -TenantFilter $Tenant -Type 'Settings' + + if (-not $Settings) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_2_3_9' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'Settings cache not found.' -Risk 'Medium' -Name "Account 'Lockout duration in seconds' is at least 60 seconds" -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication' + return + } + + $PwdSetting = $Settings | Where-Object { $_.templateId -eq '5cf42378-d67d-4f36-ba46-e8b86229381d' -or $_.displayName -eq 'Password Rule Settings' } | Select-Object -First 1 + + if (-not $PwdSetting) { + # Password Rule Settings object not present: tenant defaults apply (LockoutDurationInSeconds default = 60), which satisfies the recommendation. + $Status = 'Passed' + $Result = "Password Rule Settings not configured; the tenant default Lockout duration of 60 seconds applies, which is compliant (at least 60 seconds)." + } else { + $LockoutDuration = ($PwdSetting.values | Where-Object { $_.name -eq 'LockoutDurationInSeconds' }).value + + if ([int]$LockoutDuration -ge 60) { + $Status = 'Passed' + $Result = "Account Lockout duration is set to $([int]$LockoutDuration) seconds, which is at least 60 seconds." + } else { + $Status = 'Failed' + $Result = "Account Lockout duration is set to $([int]$LockoutDuration) seconds, which is less than the minimum recommended value of 60 seconds." + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_2_3_9' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name "Account 'Lockout duration in seconds' is at least 60 seconds" -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_2_3_9' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name "Account 'Lockout duration in seconds' is at least 60 seconds" -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication' + } +} diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_4_1.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_4_1.md index 8d76f47c147d..1f3966cfb0a6 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_4_1.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_4_1.md @@ -5,7 +5,7 @@ SSPR enables users to reset their own passwords without going through the helpde Microsoft Entra > Protection > Password reset > Properties — set Self service password reset enabled = All. **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 5.2.4.1](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.2.4.1](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_4_1.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_4_1.ps1 index 18e30ed05d89..aabadeba0f78 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_4_1.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_4_1.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_5_2_4_1 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (5.2.4.1) - 'Self service password reset enabled' SHALL be set to 'All' + Tests CIS M365 7.0.0 (5.2.4.1) - 'Self service password reset enabled' SHALL be set to 'All' #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_4_2.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_4_2.md new file mode 100644 index 000000000000..887effc3b3f1 --- /dev/null +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_4_2.md @@ -0,0 +1,13 @@ +Requiring two methods to reset a password makes self-service password reset significantly harder to abuse — an attacker who compromises a single factor cannot reset the account password on their own. + +**Remediation Action** + +Microsoft Entra admin center > Entra ID > Password reset > Authentication methods > set **Number of methods required to reset** to **2**. + +> Manual control — CIPP reports this as Informational for manual review. + +**Links** +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.2.4.2](https://www.cisecurity.org/benchmark/microsoft_365) + + +%TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_4_2.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_4_2.ps1 new file mode 100644 index 000000000000..1e7f73aad975 --- /dev/null +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_4_2.ps1 @@ -0,0 +1,9 @@ +function Invoke-CippTestCIS_5_2_4_2 { + <# + .SYNOPSIS + Tests CIS M365 7.0.0 (5.2.4.2) - Two methods SHALL be required for password reset + #> + param($Tenant) + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_2_4_2' -TestType 'Identity' -Status 'Informational' -ResultMarkdown 'This is a manual control. Verify in the Microsoft Entra admin center > Entra ID > Password reset > Authentication methods that the Number of methods required to reset is set to 2.' -Risk 'Informational' -Name 'Two methods are required for password reset' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication' +} diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_4_3.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_4_3.md new file mode 100644 index 000000000000..250e596e0b95 --- /dev/null +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_4_3.md @@ -0,0 +1,13 @@ +Requiring users to register for SSPR at sign-in, and to periodically re-confirm their authentication information, keeps recovery contact methods accurate. Stale or attacker-controlled recovery details are a common account-takeover vector, so re-confirmation reduces the window in which they can be abused. + +**Remediation Action** + +Microsoft Entra admin center > Entra ID > Password reset > Registration > set **Require users to register when signing in** to **Yes** and configure **Number of days before users are asked to re-confirm their authentication information**. + +> Manual control — CIPP reports this as Informational for manual review. + +**Links** +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.2.4.3](https://www.cisecurity.org/benchmark/microsoft_365) + + +%TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_4_3.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_4_3.ps1 new file mode 100644 index 000000000000..0176d4c211c3 --- /dev/null +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_4_3.ps1 @@ -0,0 +1,9 @@ +function Invoke-CippTestCIS_5_2_4_3 { + <# + .SYNOPSIS + Tests CIS M365 7.0.0 (5.2.4.3) - SSPR registration and authentication re-confirmation SHALL be required + #> + param($Tenant) + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_2_4_3' -TestType 'Identity' -Status 'Informational' -ResultMarkdown "This is a manual control. Verify in the Microsoft Entra admin center > Entra ID > Password reset > Registration that 'Require users to register when signing in' is Yes and re-confirmation of authentication information is required on a defined cadence." -Risk 'Informational' -Name 'SSPR registration and authentication re-confirmation are required' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication' +} diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_4_4.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_4_4.md new file mode 100644 index 000000000000..83cf11e81ebd --- /dev/null +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_4_4.md @@ -0,0 +1,13 @@ +Notifying a user when their own password is reset gives the account owner an immediate signal if the reset was not initiated by them — an early warning of an account-takeover attempt. + +**Remediation Action** + +Microsoft Entra admin center > Entra ID > Password reset > Notifications > set **Notify users on password resets** to **Yes**. + +> Manual control — CIPP reports this as Informational for manual review. + +**Links** +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.2.4.4](https://www.cisecurity.org/benchmark/microsoft_365) + + +%TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_4_4.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_4_4.ps1 new file mode 100644 index 000000000000..b389681c9dba --- /dev/null +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_4_4.ps1 @@ -0,0 +1,9 @@ +function Invoke-CippTestCIS_5_2_4_4 { + <# + .SYNOPSIS + Tests CIS M365 7.0.0 (5.2.4.4) - Users SHALL be notified on password resets + #> + param($Tenant) + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_2_4_4' -TestType 'Identity' -Status 'Informational' -ResultMarkdown "This is a manual control. Verify in the Microsoft Entra admin center > Entra ID > Password reset > Notifications that 'Notify users on password resets' is set to Yes." -Risk 'Informational' -Name 'Users are notified on password resets' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication' +} diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_4_5.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_4_5.md new file mode 100644 index 000000000000..c73fe286dc90 --- /dev/null +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_4_5.md @@ -0,0 +1,13 @@ +Administrator password resets are high-value events. Notifying all administrators when any admin resets their password provides peer visibility, so an unexpected reset of a privileged account is noticed quickly even if the targeted admin is unavailable. + +**Remediation Action** + +Microsoft Entra admin center > Entra ID > Password reset > Notifications > set **Notify all admins when other admins reset their password** to **Yes**. + +> Manual control — CIPP reports this as Informational for manual review. + +**Links** +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.2.4.5](https://www.cisecurity.org/benchmark/microsoft_365) + + +%TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_4_5.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_4_5.ps1 new file mode 100644 index 000000000000..ee8bcd845ac8 --- /dev/null +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_2_4_5.ps1 @@ -0,0 +1,9 @@ +function Invoke-CippTestCIS_5_2_4_5 { + <# + .SYNOPSIS + Tests CIS M365 7.0.0 (5.2.4.5) - All admins SHALL be notified when other admins reset their password + #> + param($Tenant) + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_2_4_5' -TestType 'Identity' -Status 'Informational' -ResultMarkdown "This is a manual control. Verify in the Microsoft Entra admin center > Entra ID > Password reset > Notifications that 'Notify all admins when other admins reset their password' is set to Yes." -Risk 'Informational' -Name 'All admins are notified when other admins reset their password' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication' +} diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_3_1.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_3_1.md index 2a5474fce5f5..9f57a4dc018e 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_3_1.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_3_1.md @@ -1,11 +1,11 @@ -PIM moves privileged roles from "always active" to "just-in-time" — admins activate the role for a limited time, with MFA + justification, leaving an audit trail. +Permanent ("Assigned") privileged role assignments expose standing administrative access continuously, whether or not the admin is performing a task. If such an account is compromised, the attacker immediately holds full role permissions with no time boundary. Privileged roles should instead be granted as PIM *eligible* and activated just-in-time, with only approved break-glass accounts (no more than two, in the Global Administrator role) holding a permanent assignment. **Remediation Action** -Microsoft Entra > Identity governance > Privileged Identity Management — assign privileged roles as Eligible (not Active) and configure activation settings: MFA, justification, ticket info. +Microsoft Entra > Identity governance > Privileged Identity Management > for each standing privileged assignment, set the **Assignment type** to **Eligible** (or remove it). Limit permanent Global Administrator assignments to at most two break-glass accounts, and give service principal assignments a defined end time. **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 5.3.1](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.3.1](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_3_1.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_3_1.ps1 index 935bbf9d7c76..4a3046f1caac 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_3_1.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_3_1.ps1 @@ -1,34 +1,64 @@ function Invoke-CippTestCIS_5_3_1 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (5.3.1) - 'Privileged Identity Management' SHALL be used to manage roles + Tests CIS M365 7.0.0 (5.3.1) - Privileged role assignments SHALL be activated and not assigned #> param($Tenant) try { - $Eligibility = Get-CIPPTestData -TenantFilter $Tenant -Type 'RoleEligibilitySchedules' $Active = Get-CIPPTestData -TenantFilter $Tenant -Type 'RoleAssignmentScheduleInstances' $Roles = Get-CIPPTestData -TenantFilter $Tenant -Type 'Roles' - if (-not $Roles) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_3_1' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'Roles cache not found.' -Risk 'Medium' -Name "'Privileged Identity Management' is used to manage roles" -UserImpact 'Medium' -ImplementationEffort 'High' -Category 'Privileged Access' + if (-not $Active) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_3_1' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'RoleAssignmentScheduleInstances cache not found. Please refresh the cache for this tenant.' -Risk 'High' -Name 'Privileged role assignments are activated and not assigned' -UserImpact 'Medium' -ImplementationEffort 'High' -Category 'Privileged Access' return } - $PrivRoleIds = [System.Collections.Generic.HashSet[string]]::new([string[]]$Roles.Where({ $_.isPrivileged -eq $true }).id) - $EligibleAssignmentsForPriv = $Eligibility.Where({ $PrivRoleIds.Contains($_.roleDefinitionId) }) + $GaTemplateId = '62e90394-69f5-4237-9190-012177145e10' - if ($EligibleAssignmentsForPriv -and $EligibleAssignmentsForPriv.Count -gt 0) { + # Map roleDefinitionId (equals roleTemplateId for built-in roles) to a display name. + $RoleNames = @{} + foreach ($r in $Roles) { + if ($r.roleTemplateId) { $RoleNames[$r.roleTemplateId] = $r.displayName } + } + + # Privileged roles whose active assignments must be JIT-activated rather than permanently assigned. + $PrivilegedRoleNames = @( + 'Global Administrator', 'Privileged Role Administrator', 'Privileged Authentication Administrator', + 'Security Administrator', 'Exchange Administrator', 'SharePoint Administrator', 'User Administrator', + 'Conditional Access Administrator', 'Application Administrator', 'Cloud Application Administrator', + 'Hybrid Identity Administrator', 'Intune Administrator', 'Authentication Administrator', + 'Helpdesk Administrator', 'Password Administrator', 'Domain Name Administrator' + ) + + # Standing (permanent) active assignments = assignmentType 'Assigned' with no end date. + $Permanent = @($Active | Where-Object { $_.assignmentType -eq 'Assigned' -and [string]::IsNullOrEmpty($_.endDateTime) }) + + $GaPermanent = @($Permanent | Where-Object { $_.roleDefinitionId -eq $GaTemplateId }) + $OtherPrivPermanent = @($Permanent | Where-Object { + $_.roleDefinitionId -ne $GaTemplateId -and $PrivilegedRoleNames -contains $RoleNames[$_.roleDefinitionId] + }) + + $Violations = @() + if ($GaPermanent.Count -gt 2) { + $Violations += "$($GaPermanent.Count) permanent Global Administrator assignments (only up to 2 break-glass accounts may be permanently assigned)." + } + if ($OtherPrivPermanent.Count -gt 0) { + $Names = ($OtherPrivPermanent | ForEach-Object { $RoleNames[$_.roleDefinitionId] } | Sort-Object -Unique) -join ', ' + $Violations += "$($OtherPrivPermanent.Count) permanent assignment(s) in privileged roles that should use PIM activation: $Names." + } + + if ($Violations.Count -eq 0) { $Status = 'Passed' - $Result = "$($EligibleAssignmentsForPriv.Count) PIM eligible assignment(s) cover privileged roles. Confirm activation requires MFA, justification, and ticket # in the role settings." + $Result = "No non-compliant standing privileged assignments found (Global Administrator permanent assignments: $($GaPermanent.Count)/2 break-glass). Confirm any remaining privileged roles use eligible (PIM) assignments." } else { $Status = 'Failed' - $Result = 'No PIM eligible assignments found for privileged roles. PIM is not in use, or every privileged user holds an active assignment.' + $Result = "Standing privileged role assignments should be moved to PIM eligibility (activated, not permanently assigned):`n`n- " + ($Violations -join "`n- ") } - Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_3_1' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name "'Privileged Identity Management' is used to manage roles" -UserImpact 'Medium' -ImplementationEffort 'High' -Category 'Privileged Access' + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_3_1' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Privileged role assignments are activated and not assigned' -UserImpact 'Medium' -ImplementationEffort 'High' -Category 'Privileged Access' } catch { $ErrorMessage = Get-CippException -Exception $_ - Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_3_1' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name "'Privileged Identity Management' is used to manage roles" -UserImpact 'Medium' -ImplementationEffort 'High' -Category 'Privileged Access' + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_3_1' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Privileged role assignments are activated and not assigned' -UserImpact 'Medium' -ImplementationEffort 'High' -Category 'Privileged Access' } } diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_3_2.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_3_2.md index 9950c1a957db..d515f37b3721 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_3_2.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_3_2.md @@ -5,7 +5,7 @@ Guest accounts proliferate over time. A recurring access review catches stale gu Microsoft Entra > Identity governance > Access reviews > New: target = All Guests dynamic group, recurrence = quarterly, decision = remove access if not reviewed. **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 5.3.2](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.3.2](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_3_2.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_3_2.ps1 index b56cfd4362e1..edfb9d290fda 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_3_2.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_3_2.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_5_3_2 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (5.3.2) - 'Access reviews' for Guest Users SHALL be configured + Tests CIS M365 7.0.0 (5.3.2) - 'Access reviews' for Guest Users SHALL be configured #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_3_3.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_3_3.md index e0eae6ec9efe..8fe251fe22bb 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_3_3.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_3_3.md @@ -5,7 +5,7 @@ Privileged role membership should not be permanent. Recurring access reviews for Microsoft Entra > PIM > Microsoft Entra roles > Access reviews — create a recurring review per privileged role, max duration 14 days. **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 5.3.3](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.3.3](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_3_3.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_3_3.ps1 index 78a1cdcab3aa..99b8af785487 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_3_3.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_3_3.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_5_3_3 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (5.3.3) - 'Access reviews' for privileged roles SHALL be configured + Tests CIS M365 7.0.0 (5.3.3) - 'Access reviews' for privileged roles SHALL be configured #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_3_4.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_3_4.md index 2df9e34e48dd..88ca673de47c 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_3_4.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_3_4.md @@ -5,7 +5,7 @@ Approval-gated GA activation prevents a single compromised admin from activating Microsoft Entra > PIM > Microsoft Entra roles > Global Administrator > Settings: Require approval to activate = Yes, approvers = (named GA list). **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 5.3.4](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.3.4](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_3_4.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_3_4.ps1 index 251325e42275..cf04713cad8e 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_3_4.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_3_4.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_5_3_4 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (5.3.4) - Approval SHALL be required for Global Administrator role activation + Tests CIS M365 7.0.0 (5.3.4) - Approval SHALL be required for Global Administrator role activation #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_3_5.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_3_5.md index b80dcf06564f..2b5e51b2ceb5 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_3_5.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_3_5.md @@ -5,7 +5,7 @@ Privileged Role Administrator can hand out *any* Entra role. An attacker who act Microsoft Entra > PIM > Microsoft Entra roles > Privileged Role Administrator > Settings: Require approval = Yes. **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 5.3.5](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 5.3.5](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_3_5.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_3_5.ps1 index c85e72e9d0b5..1f6856805fe9 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_3_5.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_5_3_5.ps1 @@ -1,9 +1,37 @@ function Invoke-CippTestCIS_5_3_5 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (5.3.5) - Approval SHALL be required for Privileged Role Administrator activation + Tests CIS M365 7.0.0 (5.3.5) - Approval SHALL be required for Privileged Role Administrator activation #> param($Tenant) - Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_3_5' -TestType 'Identity' -Status 'Informational' -ResultMarkdown 'This is a task performed manually.' -Risk 'Informational' -Name 'Approval is required for Privileged Role Administrator activation' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Privileged Access' + try { + $Policies = Get-CIPPTestData -TenantFilter $Tenant -Type 'RoleManagementPolicies' + $Roles = Get-CIPPTestData -TenantFilter $Tenant -Type 'Roles' + + if (-not $Policies -or -not $Roles) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_3_5' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'Required cache (RoleManagementPolicies or Roles) not found.' -Risk 'High' -Name 'Approval is required for Privileged Role Administrator activation' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Privileged Access' + return + } + + $PRA = $Roles | Where-Object { $_.displayName -eq 'Privileged Role Administrator' } | Select-Object -First 1 + + $ApprovalPolicy = $Policies | Where-Object { + $_.scopeId -eq '/' -and $_.scopeType -eq 'DirectoryRole' -and + ($_.rules | Where-Object { $_.id -eq 'Approval_EndUser_Assignment' -and $_.setting.isApprovalRequired -eq $true }) + } + + if ($ApprovalPolicy) { + $Status = 'Passed' + $Result = 'A PIM role management policy requires approval for activation. Verify it is scoped to Privileged Role Administrator.' + } else { + $Status = 'Failed' + $Result = 'No PIM role management policy with isApprovalRequired = true was found. Configure approval in PIM role settings for Privileged Role Administrator.' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_3_5' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Approval is required for Privileged Role Administrator activation' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Privileged Access' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_5_3_5' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Approval is required for Privileged Role Administrator activation' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Privileged Access' + } } diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_1_1.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_1_1.md index ee19878e35b0..d2bf2b0e3b11 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_1_1.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_1_1.md @@ -7,7 +7,7 @@ Set-OrganizationConfig -AuditDisabled $false ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 6.1.1](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 6.1.1](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_1_1.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_1_1.ps1 index 1c231cde17bf..62141ce466b7 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_1_1.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_1_1.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_6_1_1 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (6.1.1) - 'AuditDisabled' organizationally SHALL be 'False' + Tests CIS M365 7.0.0 (6.1.1) - 'AuditDisabled' organizationally SHALL be 'False' #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_1_2.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_1_2.md index fc8f34726508..0771abd6d4e5 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_1_2.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_1_2.md @@ -7,7 +7,7 @@ Get-Mailbox -RecipientTypeDetails UserMailbox -ResultSize Unlimited | Set-Mailbo ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 6.1.2](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 6.1.2](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_1_2.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_1_2.ps1 index 28dbec69dc90..bbd546c7f849 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_1_2.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_1_2.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_6_1_2 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (6.1.2) - Mailbox audit actions SHALL be configured + Tests CIS M365 7.0.0 (6.1.2) - Mailbox audit actions SHALL be configured #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_1_3.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_1_3.md index dd5abf42d8df..539935603122 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_1_3.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_1_3.md @@ -7,7 +7,7 @@ Get-MailboxAuditBypassAssociation | Where-Object AuditBypassEnabled | Set-Mailbo ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 6.1.3](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 6.1.3](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_1_3.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_1_3.ps1 index c5e24dc4edae..a844b8063e6b 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_1_3.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_1_3.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_6_1_3 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (6.1.3) - 'AuditBypassEnabled' SHALL NOT be enabled on mailboxes + Tests CIS M365 7.0.0 (6.1.3) - 'AuditBypassEnabled' SHALL NOT be enabled on mailboxes #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_2_1.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_2_1.md index a93a8b2b56bb..9c3900366fde 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_2_1.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_2_1.md @@ -8,7 +8,7 @@ Set-RemoteDomain -Identity Default -AutoForwardEnabled $false ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 6.2.1](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 6.2.1](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_2_1.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_2_1.ps1 index 0903f6fecf10..f846441e17a7 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_2_1.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_2_1.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_6_2_1 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (6.2.1) - All forms of mail forwarding SHALL be blocked and/or disabled + Tests CIS M365 7.0.0 (6.2.1) - All forms of mail forwarding SHALL be blocked and/or disabled #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_2_2.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_2_2.md index d4b54e8adaf2..acc95492f2f4 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_2_2.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_2_2.md @@ -5,7 +5,7 @@ A transport rule that sets SCL to -1 bypasses spam, phish and malware filtering Audit transport rules for `SetSCL = -1`. Replace with sender-based allow lists in the Tenant Allow/Block List or, better, fix the underlying authentication issue (SPF/DKIM). **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 6.2.2](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 6.2.2](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_2_2.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_2_2.ps1 index d8bee085a30d..f795f61766d8 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_2_2.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_2_2.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_6_2_2 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (6.2.2) - Mail transport rules SHALL NOT whitelist specific domains + Tests CIS M365 7.0.0 (6.2.2) - Mail transport rules SHALL NOT whitelist specific domains #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_2_3.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_2_3.md index f73393f177a7..3376e66485e4 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_2_3.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_2_3.md @@ -7,7 +7,7 @@ Set-ExternalInOutlook -Enabled $true ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 6.2.3](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 6.2.3](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_2_3.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_2_3.ps1 index c79bae495e58..24595c2ca870 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_2_3.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_2_3.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_6_2_3 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (6.2.3) - Email from external senders SHALL be identified + Tests CIS M365 7.0.0 (6.2.3) - Email from external senders SHALL be identified #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_3_1.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_3_1.md index c205130a9caf..35048b1ec896 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_3_1.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_3_1.md @@ -13,7 +13,7 @@ Get-RoleAssignmentPolicy | ForEach-Object { ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 6.3.1](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 6.3.1](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_3_1.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_3_1.ps1 index e0218f01d6b9..8442f2171a72 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_3_1.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_3_1.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_6_3_1 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (6.3.1) - Users installing Outlook add-ins SHALL NOT be allowed + Tests CIS M365 7.0.0 (6.3.1) - Users installing Outlook add-ins SHALL NOT be allowed #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_3_2.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_3_2.md new file mode 100644 index 000000000000..92549d658b20 --- /dev/null +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_3_2.md @@ -0,0 +1,14 @@ +Outlook on the web mailbox policies control whether users can add personal email accounts (`PersonalAccountsEnabled`) and connect personal calendars (`PersonalAccountCalendarsEnabled`) in Outlook. Personal accounts bypass corporate security controls such as anti-malware scanning, DLP, Safe Links, and audit logging. Allowing them alongside the corporate mailbox enables side-channel data exfiltration and creates an ingress path for malware and phishing that bypasses tenant mail-flow protections. Both settings should be `False` on the default OWA mailbox policy. + +**Remediation Action** + +```powershell +$DefaultPolicy = Get-OwaMailboxPolicy | Where-Object { $_.IsDefault } +Set-OwaMailboxPolicy -Identity $DefaultPolicy.Identity -PersonalAccountsEnabled $false -PersonalAccountCalendarsEnabled $false +``` + +**Links** +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 6.3.2](https://www.cisecurity.org/benchmark/microsoft_365) + + +%TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_3_2.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_3_2.ps1 new file mode 100644 index 000000000000..be57cecdf92b --- /dev/null +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_3_2.ps1 @@ -0,0 +1,32 @@ +function Invoke-CippTestCIS_6_3_2 { + <# + .SYNOPSIS + Tests CIS M365 7.0.0 (6.3.2) - Ensure the ability to add personal email accounts and calendars is disabled + #> + param($Tenant) + + try { + $Owa = Get-CIPPTestData -TenantFilter $Tenant -Type 'OwaMailboxPolicy' + + if (-not $Owa) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_6_3_2' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'OwaMailboxPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'Medium' -Name 'Adding personal email accounts and calendars is disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Data Protection' + return + } + + $Default = $Owa | Where-Object { $_.Identity -eq 'OwaMailboxPolicy-Default' -or $_.IsDefault -eq $true } | Select-Object -First 1 + if (-not $Default) { $Default = $Owa | Select-Object -First 1 } + + if ($Default.PersonalAccountsEnabled -eq $false -and $Default.PersonalAccountCalendarsEnabled -eq $false) { + $Status = 'Passed' + $Result = "Adding personal email accounts and calendars is disabled on '$($Default.Identity)'." + } else { + $Status = 'Failed' + $Result = "Adding personal email accounts and/or calendars is not fully disabled on '$($Default.Identity)' (PersonalAccountsEnabled: $($Default.PersonalAccountsEnabled), PersonalAccountCalendarsEnabled: $($Default.PersonalAccountCalendarsEnabled))." + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_6_3_2' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Adding personal email accounts and calendars is disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Data Protection' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_6_3_2' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Adding personal email accounts and calendars is disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Data Protection' + } +} diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_5_1.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_5_1.md index 09aa62e2c7a6..1a170b788403 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_5_1.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_5_1.md @@ -7,7 +7,7 @@ Set-OrganizationConfig -OAuth2ClientProfileEnabled $true ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 6.5.1](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 6.5.1](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_5_1.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_5_1.ps1 index ae898e66277f..5098767f79d2 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_5_1.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_5_1.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_6_5_1 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (6.5.1) - Modern authentication for Exchange Online SHALL be enabled + Tests CIS M365 7.0.0 (6.5.1) - Modern authentication for Exchange Online SHALL be enabled #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_5_2.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_5_2.md index 1e0f63323dbd..501b00088b1a 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_5_2.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_5_2.md @@ -7,7 +7,7 @@ Set-OrganizationConfig -MailTipsAllTipsEnabled $true -MailTipsExternalRecipients ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 6.5.2](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 6.5.2](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_5_2.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_5_2.ps1 index ce53e5cee435..dd547d8d3768 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_5_2.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_5_2.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_6_5_2 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (6.5.2) - MailTips SHALL be enabled for end users + Tests CIS M365 7.0.0 (6.5.2) - MailTips SHALL be enabled for end users #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_5_3.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_5_3.md index 1b86070f3694..86d85ae29b49 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_5_3.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_5_3.md @@ -7,7 +7,7 @@ Get-OwaMailboxPolicy | Set-OwaMailboxPolicy -AdditionalStorageProvidersAvailable ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 6.5.3](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 6.5.3](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_5_3.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_5_3.ps1 index 60e2d4ac79ba..c32516345df8 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_5_3.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_5_3.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_6_5_3 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (6.5.3) - Additional storage providers SHALL be restricted in Outlook on the web + Tests CIS M365 7.0.0 (6.5.3) - Additional storage providers SHALL be restricted in Outlook on the web #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_5_4.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_5_4.md index 6b193b7f81e9..0c0eb16e57b4 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_5_4.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_5_4.md @@ -7,7 +7,7 @@ Set-TransportConfig -SmtpClientAuthenticationDisabled $true ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 6.5.4](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 6.5.4](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_5_4.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_5_4.ps1 index ad4eac06c812..871b7d1f434b 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_5_4.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_5_4.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_6_5_4 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (6.5.4) - SMTP AUTH SHALL be disabled + Tests CIS M365 7.0.0 (6.5.4) - SMTP AUTH SHALL be disabled #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_5_5.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_5_5.md index 67ef02842831..792af96b1c44 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_5_5.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_5_5.md @@ -9,7 +9,7 @@ Set-OrganizationConfig -RejectDirectSend $true ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 6.5.5](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 6.5.5](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_5_5.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_5_5.ps1 index 6ac7fa662d66..f6164d402834 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_5_5.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_6_5_5.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_6_5_5 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (6.5.5) - Direct Send submissions SHALL be rejected + Tests CIS M365 7.0.0 (6.5.5) - Direct Send submissions SHALL be rejected #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_1.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_1.md index e11b6c81b8c5..3380325cf272 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_1.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_1.md @@ -7,7 +7,7 @@ Set-SPOTenant -LegacyAuthProtocolsEnabled $false ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 7.2.1](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 7.2.1](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_1.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_1.ps1 index 9760daaa1b4a..6b7b5e2df029 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_1.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_1.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_7_2_1 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (7.2.1) - Modern authentication for SharePoint applications SHALL be required + Tests CIS M365 7.0.0 (7.2.1) - Modern authentication for SharePoint applications SHALL be required #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_10.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_10.md index 0bbade427d0e..64fab515c38b 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_10.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_10.md @@ -7,7 +7,7 @@ Set-SPOTenant -EmailAttestationRequired $true -EmailAttestationReAuthDays 15 ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 7.2.10](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 7.2.10](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_10.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_10.ps1 index c5b531c9c55b..fa03a4141a10 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_10.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_10.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_7_2_10 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (7.2.10) - Reauthentication with verification code SHALL be restricted + Tests CIS M365 7.0.0 (7.2.10) - Reauthentication with verification code SHALL be restricted #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_11.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_11.md index a55b9ee409f7..12ab51e0c8bf 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_11.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_11.md @@ -7,7 +7,7 @@ Set-SPOTenant -DefaultLinkPermission View ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 7.2.11](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 7.2.11](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_11.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_11.ps1 index 93bd496c3038..f2c3acda87f1 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_11.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_11.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_7_2_11 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (7.2.11) - SharePoint default sharing link permission SHALL be set + Tests CIS M365 7.0.0 (7.2.11) - SharePoint default sharing link permission SHALL be set #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_2.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_2.md index 66e988aecced..7e2bc4f27b00 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_2.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_2.md @@ -7,7 +7,7 @@ Set-SPOTenant -EnableAzureADB2BIntegration $true ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 7.2.2](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 7.2.2](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_2.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_2.ps1 index 79e1033d6b4b..7793827d3419 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_2.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_2.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_7_2_2 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (7.2.2) - SharePoint and OneDrive integration with Azure AD B2B SHALL be enabled + Tests CIS M365 7.0.0 (7.2.2) - SharePoint and OneDrive integration with Azure AD B2B SHALL be enabled #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_3.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_3.md index 13b84eca3ac6..917a5720cb71 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_3.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_3.md @@ -7,7 +7,7 @@ Set-SPOTenant -SharingCapability ExternalUserSharingOnly ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 7.2.3](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 7.2.3](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_3.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_3.ps1 index cb3523f4fbd5..c40c3e810dfc 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_3.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_3.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_7_2_3 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (7.2.3) - External content sharing SHALL be restricted + Tests CIS M365 7.0.0 (7.2.3) - External content sharing SHALL be restricted #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_4.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_4.md index 0d4e7fa4d7d7..4bd300045f53 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_4.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_4.md @@ -7,7 +7,7 @@ Set-SPOTenant -OneDriveSharingCapability ExternalUserSharingOnly ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 7.2.4](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 7.2.4](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_4.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_4.ps1 index d6a266f69684..e2704b020a0f 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_4.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_4.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_7_2_4 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (7.2.4) - OneDrive content sharing SHALL be restricted + Tests CIS M365 7.0.0 (7.2.4) - OneDrive content sharing SHALL be restricted #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_5.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_5.md index 4e2d9e09b5b9..33c2629b5d1a 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_5.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_5.md @@ -7,7 +7,7 @@ Set-SPOTenant -PreventExternalUsersFromResharing $true ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 7.2.5](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 7.2.5](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_5.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_5.ps1 index fe949f0edb5c..d868ccf67307 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_5.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_5.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_7_2_5 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (7.2.5) - SharePoint guest users SHALL NOT share items they don't own + Tests CIS M365 7.0.0 (7.2.5) - SharePoint guest users SHALL NOT share items they don't own #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_6.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_6.md index b1c478f0f060..03de4b4a87ef 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_6.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_6.md @@ -7,7 +7,7 @@ Set-SPOTenant -SharingDomainRestrictionMode AllowList -SharingAllowedDomainList ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 7.2.6](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 7.2.6](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_6.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_6.ps1 index 99114158bf30..8ccee7eb6525 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_6.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_6.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_7_2_6 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (7.2.6) - SharePoint external sharing SHALL be restricted + Tests CIS M365 7.0.0 (7.2.6) - SharePoint external sharing SHALL be restricted #> param($Tenant) @@ -14,19 +14,25 @@ function Invoke-CippTestCIS_7_2_6 { } $Cfg = $SPO | Select-Object -First 1 + $Capability = $Cfg.SharingCapability $Mode = $Cfg.SharingDomainRestrictionMode $AllowList = $Cfg.SharingAllowedDomainList $BlockList = $Cfg.SharingBlockedDomainList - $Pass = ($Mode -eq 'AllowList' -and -not [string]::IsNullOrWhiteSpace($AllowList)) -or + $Pass = $Capability -eq 'Disabled' -or + ($Mode -eq 'AllowList' -and -not [string]::IsNullOrWhiteSpace($AllowList)) -or ($Mode -eq 'BlockList' -and -not [string]::IsNullOrWhiteSpace($BlockList)) if ($Pass) { $Status = 'Passed' - $Result = "External sharing is restricted by domain list (mode: $Mode)." + if ($Capability -eq 'Disabled') { + $Result = 'External sharing is fully disabled (SharingCapability: Disabled).' + } else { + $Result = "External sharing is restricted by domain list (mode: $Mode)." + } } else { $Status = 'Failed' - $Result = "External sharing is not restricted by domain list (SharingDomainRestrictionMode: $Mode)." + $Result = "External sharing is not restricted (SharingCapability: $Capability, SharingDomainRestrictionMode: $Mode)." } Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_7_2_6' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'SharePoint external sharing is restricted' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'External Collaboration' diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_7.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_7.md index 1707d67e3a4a..c18bc04468c0 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_7.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_7.md @@ -7,7 +7,7 @@ Set-SPOTenant -DefaultSharingLinkType Direct ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 7.2.7](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 7.2.7](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_7.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_7.ps1 index e328daf54e80..7622e8f0968d 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_7.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_7.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_7_2_7 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (7.2.7) - Link sharing SHALL be restricted in SharePoint and OneDrive + Tests CIS M365 7.0.0 (7.2.7) - Link sharing SHALL be restricted in SharePoint and OneDrive #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_8.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_8.md index 4fd48541215c..29f324bfe146 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_8.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_8.md @@ -5,7 +5,7 @@ Limiting external sharing to a security group lets you control who in the organi SharePoint admin centre > Policies > Sharing > Limit external sharing by security group. **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 7.2.8](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 7.2.8](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_8.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_8.ps1 index 98a929b52950..801f90d21b02 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_8.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_8.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_7_2_8 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (7.2.8) - External sharing SHALL be restricted by security group + Tests CIS M365 7.0.0 (7.2.8) - External sharing SHALL be restricted by security group #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_9.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_9.md index a28458deef86..33d7adad6e30 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_9.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_9.md @@ -7,7 +7,7 @@ Set-SPOTenant -ExternalUserExpirationRequired $true -ExternalUserExpireInDays 30 ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 7.2.9](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 7.2.9](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_9.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_9.ps1 index 80026e166328..ce51c0e55b27 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_9.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_2_9.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_7_2_9 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (7.2.9) - Guest access to a site or OneDrive SHALL expire automatically + Tests CIS M365 7.0.0 (7.2.9) - Guest access to a site or OneDrive SHALL expire automatically #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_3_1.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_3_1.md index 3fcb38f35791..e28376c25f8e 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_3_1.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_3_1.md @@ -7,7 +7,7 @@ Set-SPOTenant -DisallowInfectedFileDownload $true ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 7.3.1](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 7.3.1](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_3_1.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_3_1.ps1 index ae5b57d105c6..aa143eaa2521 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_3_1.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_3_1.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_7_3_1 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (7.3.1) - Office 365 SharePoint infected files SHALL be disallowed for download + Tests CIS M365 7.0.0 (7.3.1) - Office 365 SharePoint infected files SHALL be disallowed for download #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_3_2.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_3_2.md deleted file mode 100644 index 1537d891f930..000000000000 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_3_2.md +++ /dev/null @@ -1,16 +0,0 @@ -Allowing OneDrive to sync to any device puts company data on personal / unmanaged endpoints. Restrict sync to AD-joined or compliant devices. - -**Remediation Action** - -```powershell -# Hybrid AD -Set-SPOTenantSyncClientRestriction -Enable -DomainGuids '' -# Entra-only -Set-SPOTenant -ConditionalAccessPolicy AllowLimitedAccess -``` - -**Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 7.3.2](https://www.cisecurity.org/benchmark/microsoft_365) - - -%TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_3_2.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_3_2.ps1 deleted file mode 100644 index c239e32f08a3..000000000000 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_7_3_2.ps1 +++ /dev/null @@ -1,36 +0,0 @@ -function Invoke-CippTestCIS_7_3_2 { - <# - .SYNOPSIS - Tests CIS M365 6.0.1 (7.3.2) - OneDrive sync SHALL be restricted for unmanaged devices - #> - param($Tenant) - - try { - $Sync = Get-CIPPTestData -TenantFilter $Tenant -Type 'SPOTenantSyncClientRestriction' - $SPO = Get-CIPPTestData -TenantFilter $Tenant -Type 'SPOTenant' - - if (-not $Sync -and -not $SPO) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_7_3_2' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'Required cache (SPOTenantSyncClientRestriction or SPOTenant) not found. Please refresh the cache for this tenant.' -Risk 'Medium' -Name 'OneDrive sync is restricted for unmanaged devices' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Device Management' - return - } - - $S = $Sync | Select-Object -First 1 - $T = $SPO | Select-Object -First 1 - - $DomainRestricted = $S.TenantRestrictionEnabled -eq $true -and -not [string]::IsNullOrWhiteSpace($S.AllowedDomainList) - $CARestricted = $T.ConditionalAccessPolicy -in @('AllowLimitedAccess', 'BlockAccess') - - if ($DomainRestricted -or $CARestricted) { - $Status = 'Passed' - $Result = "OneDrive sync is restricted for unmanaged devices.`n`n- TenantRestrictionEnabled: $($S.TenantRestrictionEnabled)`n- ConditionalAccessPolicy: $($T.ConditionalAccessPolicy)" - } else { - $Status = 'Failed' - $Result = "OneDrive sync is not restricted for unmanaged devices.`n`n- TenantRestrictionEnabled: $($S.TenantRestrictionEnabled)`n- ConditionalAccessPolicy: $($T.ConditionalAccessPolicy)" - } - - Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_7_3_2' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'OneDrive sync is restricted for unmanaged devices' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Device Management' - } catch { - $ErrorMessage = Get-CippException -Exception $_ - Add-CippTestResult -TenantFilter $Tenant -TestId 'CIS_7_3_2' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'OneDrive sync is restricted for unmanaged devices' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Device Management' - } -} diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_1_1.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_1_1.md index 94ee7ff3019f..002370381fc5 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_1_1.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_1_1.md @@ -7,7 +7,7 @@ Set-CsTeamsClientConfiguration -Identity Global -AllowDropbox $false -AllowBox $ ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 8.1.1](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 8.1.1](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_1_1.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_1_1.ps1 index 2183391c5117..01b08e8f81bf 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_1_1.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_1_1.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_8_1_1 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (8.1.1) - External file sharing in Teams SHALL be enabled for only approved cloud storage services + Tests CIS M365 7.0.0 (8.1.1) - External file sharing in Teams SHALL be enabled for only approved cloud storage services #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_1_2.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_1_2.md index 3dcb53b2058f..6ff9727846a9 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_1_2.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_1_2.md @@ -7,7 +7,7 @@ Set-CsTeamsClientConfiguration -Identity Global -AllowEmailIntoChannel $false ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 8.1.2](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 8.1.2](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_1_2.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_1_2.ps1 index 13fe3fd55e98..c5aa1193b170 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_1_2.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_1_2.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_8_1_2 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (8.1.2) - Users SHALL NOT be able to send emails to a channel email address + Tests CIS M365 7.0.0 (8.1.2) - Users SHALL NOT be able to send emails to a channel email address #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_2_1.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_2_1.md index 10dc6c02f674..59bb0e8c0296 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_2_1.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_2_1.md @@ -7,7 +7,7 @@ Set-CsTenantFederationConfiguration -AllowedDomains (New-CsEdgeAllowList -Allowe ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 8.2.1](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 8.2.1](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_2_1.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_2_1.ps1 index 4d5af185378f..dc196f8c0935 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_2_1.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_2_1.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_8_2_1 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (8.2.1) - External domains SHALL be restricted in the Teams admin center + Tests CIS M365 7.0.0 (8.2.1) - External domains SHALL be restricted in the Teams admin center #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_2_2.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_2_2.md index 47a21a17b669..9eb6be7c2519 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_2_2.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_2_2.md @@ -7,7 +7,7 @@ Set-CsExternalAccessPolicy -Identity Global -EnableTeamsConsumerAccess $false ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 8.2.2](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 8.2.2](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_2_2.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_2_2.ps1 index 3c50d72d194d..a07cc190ecfe 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_2_2.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_2_2.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_8_2_2 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (8.2.2) - Communication with unmanaged Teams users SHALL be disabled + Tests CIS M365 7.0.0 (8.2.2) - Communication with unmanaged Teams users SHALL be disabled #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_2_3.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_2_3.md index 6eec9469075d..ad4322554226 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_2_3.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_2_3.md @@ -7,7 +7,7 @@ Set-CsTeamsMessagingPolicy -Identity Global -UseB2BInvitesToAddExternalUsers $fa ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 8.2.3](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 8.2.3](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_2_3.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_2_3.ps1 index e459d2daea9f..e36568fc0ea2 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_2_3.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_2_3.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_8_2_3 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (8.2.3) - External Teams users SHALL NOT be able to initiate conversations + Tests CIS M365 7.0.0 (8.2.3) - External Teams users SHALL NOT be able to initiate conversations #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_2_4.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_2_4.md index 615cdbbb0c89..bff8c9147b1a 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_2_4.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_2_4.md @@ -5,7 +5,7 @@ Trial Teams tenants are short-lived, low-friction accounts attackers spin up to Teams admin centre > Users > External access — uncheck the trial tenants option. **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 8.2.4](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 8.2.4](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_2_4.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_2_4.ps1 index 6db74cdf7292..cbc89fadd982 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_2_4.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_2_4.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_8_2_4 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (8.2.4) - Organization SHALL NOT communicate with accounts in trial Teams tenants + Tests CIS M365 7.0.0 (8.2.4) - Organization SHALL NOT communicate with accounts in trial Teams tenants #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_4_1.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_4_1.md index c495e0ed233b..6e57d4fa9d2e 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_4_1.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_4_1.md @@ -5,7 +5,7 @@ Teams apps run with broad permissions. CIS recommends an explicit allow-list of Teams admin centre > Teams apps > Permission policies > Global — set Third-party apps to Block all apps (or Allow specific apps and add the approved list). **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 8.4.1](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 8.4.1](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_4_1.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_4_1.ps1 index e8c8a1f7ba57..1f2b30d84f5e 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_4_1.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_4_1.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_8_4_1 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (8.4.1) - App permission policies SHALL be configured + Tests CIS M365 7.0.0 (8.4.1) - App permission policies SHALL be configured #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_1.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_1.md index e376a84995bf..624cf4a72fae 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_1.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_1.md @@ -7,7 +7,7 @@ Set-CsTeamsMeetingPolicy -Identity Global -AllowAnonymousUsersToJoinMeeting $fal ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 8.5.1](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 8.5.1](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_1.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_1.ps1 index f675ac2fbd29..528b468be1fc 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_1.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_1.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_8_5_1 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (8.5.1) - Anonymous users SHALL NOT be able to join a meeting + Tests CIS M365 7.0.0 (8.5.1) - Anonymous users SHALL NOT be able to join a meeting #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_2.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_2.md index c3b94088516a..0328f93e7301 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_2.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_2.md @@ -7,7 +7,7 @@ Set-CsTeamsMeetingPolicy -Identity Global -AllowAnonymousUsersToStartMeeting $fa ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 8.5.2](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 8.5.2](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_2.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_2.ps1 index d377ff58c07d..7b4b67d90330 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_2.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_2.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_8_5_2 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (8.5.2) - Anonymous users and dial-in callers SHALL NOT be able to start a meeting + Tests CIS M365 7.0.0 (8.5.2) - Anonymous users and dial-in callers SHALL NOT be able to start a meeting #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_3.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_3.md index 2c1fb78e98f8..d18a1cebb640 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_3.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_3.md @@ -7,7 +7,7 @@ Set-CsTeamsMeetingPolicy -Identity Global -AutoAdmittedUsers EveryoneInCompanyEx ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 8.5.3](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 8.5.3](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_3.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_3.ps1 index efd8438e341e..0ecac7126635 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_3.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_3.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_8_5_3 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (8.5.3) - Only people in my org SHALL be able to bypass the lobby + Tests CIS M365 7.0.0 (8.5.3) - Only people in my org SHALL be able to bypass the lobby #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_4.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_4.md index 42e6f720f3b8..5fb0fe9bb6ea 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_4.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_4.md @@ -7,7 +7,7 @@ Set-CsTeamsMeetingPolicy -Identity Global -AllowPSTNUsersToBypassLobby $false ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 8.5.4](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 8.5.4](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_4.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_4.ps1 index bae0c1dc803c..7fda70810fe5 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_4.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_4.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_8_5_4 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (8.5.4) - Users dialing in SHALL NOT bypass the lobby + Tests CIS M365 7.0.0 (8.5.4) - Users dialing in SHALL NOT bypass the lobby #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_5.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_5.md index 6bbf75ef50aa..889fd01f2ad5 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_5.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_5.md @@ -7,7 +7,7 @@ Set-CsTeamsMeetingPolicy -Identity Global -MeetingChatEnabledType EnabledExceptA ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 8.5.5](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 8.5.5](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_5.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_5.ps1 index 7a8af5dc8a9d..0d7073d39a68 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_5.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_5.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_8_5_5 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (8.5.5) - Meeting chat SHALL NOT allow anonymous users + Tests CIS M365 7.0.0 (8.5.5) - Meeting chat SHALL NOT allow anonymous users #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_6.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_6.md index 536fff20ff31..3ec79f51bc53 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_6.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_6.md @@ -7,7 +7,7 @@ Set-CsTeamsMeetingPolicy -Identity Global -DesignatedPresenterRoleMode Organizer ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 8.5.6](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 8.5.6](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_6.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_6.ps1 index 14a7c77b88ec..2b91d80b8c61 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_6.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_6.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_8_5_6 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (8.5.6) - Only organizers and co-organizers SHALL be able to present + Tests CIS M365 7.0.0 (8.5.6) - Only organizers and co-organizers SHALL be able to present #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_7.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_7.md index 7dadaa413c2c..f0ab2e3a22ed 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_7.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_7.md @@ -7,7 +7,7 @@ Set-CsTeamsMeetingPolicy -Identity Global -AllowExternalParticipantGiveRequestCo ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 8.5.7](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 8.5.7](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_7.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_7.ps1 index 33f080c781c6..b3c42489c382 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_7.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_7.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_8_5_7 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (8.5.7) - External participants SHALL NOT give or request control + Tests CIS M365 7.0.0 (8.5.7) - External participants SHALL NOT give or request control #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_8.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_8.md index b159420d0db4..f5acb354850e 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_8.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_8.md @@ -7,7 +7,7 @@ Set-CsTeamsMeetingPolicy -Identity Global -AllowExternalNonTrustedMeetingChat $f ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 8.5.8](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 8.5.8](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_8.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_8.ps1 index 77498d547504..98ec0e7fbb39 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_8.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_8.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_8_5_8 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (8.5.8) - External meeting chat SHALL be off + Tests CIS M365 7.0.0 (8.5.8) - External meeting chat SHALL be off #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_9.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_9.md index 909a06c474fc..78593fd2875c 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_9.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_9.md @@ -7,7 +7,7 @@ Set-CsTeamsMeetingPolicy -Identity Global -AllowCloudRecording $false ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 8.5.9](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 8.5.9](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_9.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_9.ps1 index 950209604941..b76f856ff1e0 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_9.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_5_9.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_8_5_9 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (8.5.9) - Meeting recording SHALL be off by default + Tests CIS M365 7.0.0 (8.5.9) - Meeting recording SHALL be off by default #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_6_1.md b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_6_1.md index 7d4cba74f1be..30c4b5291956 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_6_1.md +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_6_1.md @@ -8,7 +8,7 @@ Set-ReportSubmissionPolicy -Identity DefaultReportSubmissionPolicy -ReportJunkTo ``` **Links** -- [CIS Microsoft 365 Foundations Benchmark v6.0.1 - 8.6.1](https://www.cisecurity.org/benchmark/microsoft_365) +- [CIS Microsoft 365 Foundations Benchmark v7.0.0 - 8.6.1](https://www.cisecurity.org/benchmark/microsoft_365) %TestResult% diff --git a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_6_1.ps1 b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_6_1.ps1 index 7a1beb834c08..000fc2088923 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_6_1.ps1 +++ b/Modules/CIPPTests/Public/Tests/CIS/Identity/Invoke-CippTestCIS_8_6_1.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestCIS_8_6_1 { <# .SYNOPSIS - Tests CIS M365 6.0.1 (8.6.1) - Users SHALL be able to report security concerns in Teams + Tests CIS M365 7.0.0 (8.6.1) - Users SHALL be able to report security concerns in Teams #> param($Tenant) diff --git a/Modules/CIPPTests/Public/Tests/CIS/report.json b/Modules/CIPPTests/Public/Tests/CIS/report.json index 25020866c7a6..ab280bb34aa7 100644 --- a/Modules/CIPPTests/Public/Tests/CIS/report.json +++ b/Modules/CIPPTests/Public/Tests/CIS/report.json @@ -1,7 +1,7 @@ { - "name": "CIS Microsoft 365 Foundations Benchmark v6.0.1", - "description": "Center for Internet Security (CIS) Microsoft 365 Foundations Benchmark v6.0.1 — prescriptive technical baseline for securely configuring a Microsoft 365 tenant across the M365 admin centre, Defender, Purview, Intune, Entra, Exchange Online, SharePoint, and Teams.", - "version": "6.0.1", + "name": "CIS Microsoft 365 Foundations Benchmark v7.0.0", + "description": "Center for Internet Security (CIS) Microsoft 365 Foundations Benchmark v7.0.0 — prescriptive technical baseline for securely configuring a Microsoft 365 tenant across the M365 admin centre, Defender, Purview, Intune, Entra, Exchange Online, SharePoint, and Teams.", + "version": "7.0.0", "source": "https://www.cisecurity.org/benchmark/microsoft_365", "category": "CIS Security Baselines", "IdentityTests": [ @@ -40,9 +40,11 @@ "CIS_2_4_2", "CIS_2_4_3", "CIS_2_4_4", + "CIS_2_4_5", "CIS_3_1_1", "CIS_3_2_1", "CIS_3_2_2", + "CIS_3_2_3", "CIS_3_3_1", "CIS_4_1", "CIS_4_2", @@ -54,6 +56,8 @@ "CIS_5_1_2_6", "CIS_5_1_3_1", "CIS_5_1_3_2", + "CIS_5_1_3_3", + "CIS_5_1_3_4", "CIS_5_1_4_1", "CIS_5_1_4_2", "CIS_5_1_4_3", @@ -62,6 +66,10 @@ "CIS_5_1_4_6", "CIS_5_1_5_1", "CIS_5_1_5_2", + "CIS_5_1_5_3", + "CIS_5_1_5_4", + "CIS_5_1_5_5", + "CIS_5_1_5_6", "CIS_5_1_6_1", "CIS_5_1_6_2", "CIS_5_1_6_3", @@ -78,6 +86,11 @@ "CIS_5_2_2_10", "CIS_5_2_2_11", "CIS_5_2_2_12", + "CIS_5_2_2_13", + "CIS_5_2_2_14", + "CIS_5_2_2_15", + "CIS_5_2_2_16", + "CIS_5_2_2_17", "CIS_5_2_3_1", "CIS_5_2_3_2", "CIS_5_2_3_3", @@ -85,7 +98,14 @@ "CIS_5_2_3_5", "CIS_5_2_3_6", "CIS_5_2_3_7", + "CIS_5_2_3_8", + "CIS_5_2_3_9", + "CIS_5_2_3_10", "CIS_5_2_4_1", + "CIS_5_2_4_2", + "CIS_5_2_4_3", + "CIS_5_2_4_4", + "CIS_5_2_4_5", "CIS_5_3_1", "CIS_5_3_2", "CIS_5_3_3", @@ -98,6 +118,7 @@ "CIS_6_2_2", "CIS_6_2_3", "CIS_6_3_1", + "CIS_6_3_2", "CIS_6_5_1", "CIS_6_5_2", "CIS_6_5_3", @@ -115,7 +136,6 @@ "CIS_7_2_10", "CIS_7_2_11", "CIS_7_3_1", - "CIS_7_3_2", "CIS_8_1_1", "CIS_8_1_2", "CIS_8_2_1", From 5df315b6c86356251eb341fbdb6aac773daf9e7d Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sat, 6 Jun 2026 21:00:02 +0200 Subject: [PATCH 158/202] improved open api spec for ai --- openapi.json | 18148 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 12786 insertions(+), 5362 deletions(-) diff --git a/openapi.json b/openapi.json index a0c8a4fa087d..7fbc0152a540 100644 --- a/openapi.json +++ b/openapi.json @@ -149,7 +149,7 @@ "paths": { "/api/AddAPDevice": { "post": { - "summary": "AddAPDevice", + "summary": "Adds Autopilot devices to a tenant via Partner Center API", "tags": [ "Endpoint > Autopilot" ], @@ -170,19 +170,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "description": "Adds Autopilot devices to a tenant via Partner Center API\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", + "description": "Adds Autopilot devices to a tenant via Partner Center API", "x-cipp-role": "Endpoint.Autopilot.ReadWrite", "requestBody": { "required": true, @@ -233,13 +233,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -253,51 +253,83 @@ "schema": { "type": "object", "properties": { + "CustomSubject": { + "type": "string" + }, "actions": { "type": "array", "items": { "type": "string" - } + }, + "description": "actions parameter" }, "AlertComment": { - "type": "string" + "type": "string", + "description": "AlertComment parameter" }, "command": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "command parameter" }, "conditions": { - "type": "string" + "type": "string", + "description": "conditions parameter" }, "excludedTenants": { - "type": "string" + "type": "string", + "description": "excludedTenants parameter" }, "logbook": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "logbook parameter" }, "postExecution": { "type": "array", "items": { "type": "string" - } + }, + "description": "postExecution parameter" }, "preset": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "preset parameter" }, "recurrence": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "recurrence parameter" }, "RowKey": { - "type": "string" + "type": "string", + "description": "RowKey parameter" }, "startDateTime": { "type": "string", - "format": "date-time" + "format": "date-time", + "description": "startDateTime parameter" }, "tenantFilter": { - "type": "string" + "type": "string", + "description": "tenantFilter parameter" }, "count": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: count" } }, "required": [ @@ -309,6 +341,180 @@ } } }, + "/api/AddAppTemplate": { + "post": { + "summary": "AddAppTemplate", + "tags": [ + "Endpoint > Applications" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.Application.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "AcceptLicense": { + "type": "boolean" + }, + "applicationName": { + "type": "string" + }, + "apps": { + "type": "string" + }, + "appType": { + "$ref": "#/components/schemas/LabelValue" + }, + "arch": { + "type": "boolean" + }, + "AssignTo": { + "type": "string", + "enum": [ + "On", + "allLicensedUsers", + "AllDevices", + "AllDevicesAndUsers", + "customGroup" + ] + }, + "customArguments": { + "type": "string" + }, + "customGroup": { + "type": "string" + }, + "customRepo": { + "type": "string" + }, + "customXml": { + "type": "string" + }, + "description": { + "type": "string" + }, + "detectionFile": { + "type": "string" + }, + "detectionPath": { + "type": "string" + }, + "detectionScript": { + "type": "string" + }, + "DisableRestart": { + "type": "boolean" + }, + "displayName": { + "type": "string" + }, + "enforceSignatureCheck": { + "type": "boolean" + }, + "excludedApps": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LabelValue" + } + }, + "excludeGroup": { + "type": "string" + }, + "GUID": { + "type": "string" + }, + "InstallAsSystem": { + "type": "boolean" + }, + "InstallationIntent": { + "type": "boolean" + }, + "installScript": { + "type": "string" + }, + "languages": { + "$ref": "#/components/schemas/LabelValue" + }, + "packagename": { + "type": "string" + }, + "packageSearch": { + "$ref": "#/components/schemas/LabelValue" + }, + "publisher": { + "type": "string" + }, + "RemoveVersions": { + "type": "boolean" + }, + "rmmname": { + "$ref": "#/components/schemas/LabelValue" + }, + "runAs32Bit": { + "type": "boolean" + }, + "searchQuery": { + "type": "string" + }, + "SharedComputerActivation": { + "type": "boolean" + }, + "templateDescription": { + "type": "string" + }, + "templateName": { + "type": "string" + }, + "uninstallScript": { + "type": "string" + }, + "updateChannel": { + "$ref": "#/components/schemas/LabelValue" + }, + "useCustomXml": { + "type": "boolean" + }, + "useDetectionScript": { + "type": "boolean" + } + } + } + } + } + } + } + }, "/api/AddAssignmentFilter": { "post": { "summary": "AddAssignmentFilter", @@ -332,13 +538,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -353,26 +559,32 @@ "type": "object", "properties": { "displayName": { - "type": "string" + "type": "string", + "description": "(auto) from CippAddAssignmentFilterForm.jsx" }, "description": { - "type": "string" + "type": "string", + "description": "(auto) from CippAddAssignmentFilterForm.jsx" }, "assignmentFilterManagementType": { "type": "string", "enum": [ "devices", "apps" - ] + ], + "description": "(auto) from CippAddAssignmentFilterForm.jsx" }, "platform": { - "type": "string" + "type": "string", + "description": "(auto) from CippAddAssignmentFilterForm.jsx" }, "rule": { - "type": "string" + "type": "string", + "description": "(auto) from CippAddAssignmentFilterForm.jsx" }, "tenantFilter": { - "type": "string" + "type": "string", + "description": "(auto) from API scan" } }, "required": [ @@ -407,13 +619,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -428,26 +640,32 @@ "type": "object", "properties": { "GUID": { - "type": "string" + "type": "string", + "description": "(auto) from CippAddAssignmentFilterTemplateForm.jsx" }, "assignmentFilterManagementType": { "type": "string", "enum": [ "devices", "apps" - ] + ], + "description": "(auto) from CippAddAssignmentFilterTemplateForm.jsx" }, "platform": { - "type": "string" + "type": "string", + "description": "(auto) from CippAddAssignmentFilterTemplateForm.jsx" }, "rule": { - "type": "string" + "type": "string", + "description": "(auto) from CippAddAssignmentFilterTemplateForm.jsx" }, "displayname": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: displayname" }, "Description": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: Description" } } } @@ -479,13 +697,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -571,13 +789,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -592,14 +810,16 @@ "type": "object", "properties": { "name": { - "type": "string" + "type": "string", + "description": "name parameter" }, "style": { "type": "string", "enum": [ "Tenant", "Table" - ] + ], + "description": "style parameter" } } } @@ -631,13 +851,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -652,10 +872,12 @@ "type": "object", "properties": { "CreateGroups": { - "type": "boolean" + "type": "boolean", + "description": "CreateGroups parameter" }, "DisableSD": { - "type": "boolean" + "type": "boolean", + "description": "DisableSD parameter" }, "NewState": { "type": "string", @@ -664,13 +886,16 @@ "Enabled", "Disabled", "enabledForReportingButNotEnforced" - ] + ], + "description": "NewState parameter" }, "overwrite": { - "type": "boolean" + "type": "boolean", + "description": "overwrite parameter" }, "RawJSON": { - "type": "string" + "type": "string", + "description": "RawJSON parameter" }, "replacename": { "type": "string", @@ -678,13 +903,20 @@ "leave", "displayName", "AllUsers" - ] + ], + "description": "replacename parameter" }, "TemplateList": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "TemplateList parameter" }, "tenantFilter": { - "type": "string" + "type": "string", + "description": "tenantFilter parameter" } }, "required": [ @@ -719,13 +951,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -740,13 +972,20 @@ "type": "object", "properties": { "name": { - "type": "string" + "type": "string", + "description": "name parameter" }, "policySource": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "policySource parameter" }, "tenantFilter": { - "type": "string" + "type": "string", + "description": "tenantFilter parameter" } }, "required": [ @@ -781,13 +1020,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -802,37 +1041,48 @@ "type": "object", "properties": { "ApplicationName": { - "type": "string" + "type": "string", + "description": "ApplicationName parameter" }, "AssignTo": { - "type": "string" + "type": "string", + "description": "AssignTo parameter" }, "customArguments": { - "type": "string" + "type": "string", + "description": "customArguments parameter" }, "CustomGroup": { - "type": "string" + "type": "string", + "description": "CustomGroup parameter" }, "description": { - "type": "string" + "type": "string", + "description": "description parameter" }, "DisableRestart": { - "type": "string" + "type": "string", + "description": "DisableRestart parameter" }, "InstallAsSystem": { - "type": "string" + "type": "string", + "description": "InstallAsSystem parameter" }, "InstallationIntent": { - "type": "string" + "type": "string", + "description": "InstallationIntent parameter" }, "PackageName": { - "type": "string" + "type": "string", + "description": "PackageName parameter" }, "selectedTenants": { - "type": "string" + "type": "string", + "description": "selectedTenants parameter" }, "CustomRepo": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: CustomRepo" } } } @@ -864,13 +1114,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -885,13 +1135,20 @@ "type": "object", "properties": { "PowerShellCommand": { - "type": "string" + "type": "string", + "description": "PowerShellCommand parameter" }, "selectedTenants": { - "type": "string" + "type": "string", + "description": "selectedTenants parameter" }, "TemplateList": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "TemplateList parameter" } } } @@ -923,13 +1180,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -979,13 +1236,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -1000,55 +1257,72 @@ "type": "object", "properties": { "Company": { - "type": "string" + "type": "string", + "description": "(auto) API uses 'Company', frontend sends 'companyName' \u2014 likely the same field (similarity=0.78). Verify and correct type." }, "Title": { - "type": "string" + "type": "string", + "description": "(auto) API uses 'Title', frontend sends 'jobTitle' \u2014 likely the same field (similarity=0.77). Verify and correct type." }, "phone": { - "type": "string" + "type": "string", + "description": "(auto) API uses 'phone', frontend sends 'businessPhone' \u2014 likely the same field (similarity=0.56). Verify and correct type." }, "displayName": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: displayName" }, "State": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: State" }, "tenantid": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: tenantid" }, "lastName": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: lastName" }, "PostalCode": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: PostalCode" }, "StreetAddress": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: StreetAddress" }, "mobilePhone": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: mobilePhone" }, "firstName": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: firstName" }, "email": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: email" }, "mailTip": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: mailTip" }, "website": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: website" }, "City": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: City" }, "hidefromGAL": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: hidefromGAL" }, "CountryOrRegion": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: CountryOrRegion" } } } @@ -1080,13 +1354,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -1104,7 +1378,8 @@ "type": "object", "properties": { "displayName": { - "type": "string" + "type": "string", + "description": "Template display name; also used as 'name' key internally" }, "firstName": { "type": "string" @@ -1116,7 +1391,8 @@ "type": "string" }, "hidefromGAL": { - "type": "boolean" + "type": "boolean", + "description": "Hide from Global Address List" }, "streetAddress": { "type": "string" @@ -1131,7 +1407,8 @@ "type": "string" }, "country": { - "type": "string" + "type": "string", + "description": "Country code (2-letter ISO code extracted from autocomplete object by formatter)" }, "companyName": { "type": "string" @@ -1158,6 +1435,113 @@ } } }, + "/api/AddCustomScript": { + "post": { + "summary": "AddCustomScript", + "tags": [ + "Tools > Custom-Scripts" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Tests.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Action": { + "type": "string" + }, + "AlertOnFailure": { + "type": "string" + }, + "AlertStatuses": { + "type": "string" + }, + "Category": { + "type": "string" + }, + "Description": { + "type": "string" + }, + "Enabled": { + "type": "boolean" + }, + "ImplementationEffort": { + "type": "string" + }, + "MarkdownTemplate": { + "type": "string" + }, + "Pillar": { + "type": "string" + }, + "RestoreToVersion": { + "type": "string" + }, + "ResultMode": { + "type": "string" + }, + "ResultSchema": { + "type": "string" + }, + "ReturnType": { + "type": "string" + }, + "Risk": { + "type": "string" + }, + "ScriptContent": { + "type": "string" + }, + "ScriptGuid": { + "type": "string" + }, + "ScriptName": { + "type": "string" + }, + "TestParameters": { + "type": "string" + }, + "UserImpact": { + "type": "string" + } + } + } + } + } + } + } + }, "/api/AddDefenderDeployment": { "post": { "summary": "AddDefenderDeployment", @@ -1181,13 +1565,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -1203,35 +1587,45 @@ "properties": { "Compliance": { "type": "object", + "description": "Compliance parameter", "properties": { "AllowMEMEnforceCompliance": { - "type": "boolean" + "type": "boolean", + "description": "Compliance.AllowMEMEnforceCompliance parameter" }, "AppSync": { - "type": "boolean" + "type": "boolean", + "description": "Compliance.AppSync parameter" }, "BlockunsupportedOS": { - "type": "boolean" + "type": "boolean", + "description": "Compliance.BlockunsupportedOS parameter" }, "ConnectAndroid": { - "type": "boolean" + "type": "boolean", + "description": "Compliance.ConnectAndroid parameter" }, "ConnectAndroidCompliance": { - "type": "boolean" + "type": "boolean", + "description": "Compliance.ConnectAndroidCompliance parameter" }, "ConnectIos": { - "type": "boolean" + "type": "boolean", + "description": "Compliance.ConnectIos parameter" }, "ConnectIosCompliance": { - "type": "boolean" + "type": "boolean", + "description": "Compliance.ConnectIosCompliance parameter" }, "ConnectWindows": { - "type": "boolean" + "type": "boolean", + "description": "Compliance.ConnectWindows parameter" } } }, "ASR": { "type": "object", + "description": "ASR parameter", "properties": { "AssignTo": { "type": "string", @@ -1240,61 +1634,80 @@ "allLicensedUsers", "AllDevices", "AllDevicesAndUsers" - ] + ], + "description": "ASR.AssignTo parameter" }, "BlockAdobeChild": { - "type": "boolean" + "type": "boolean", + "description": "ASR.BlockAdobeChild parameter" }, "BlockCredentialStealing": { - "type": "boolean" + "type": "boolean", + "description": "ASR.BlockCredentialStealing parameter" }, "BlockExesMail": { - "type": "boolean" + "type": "boolean", + "description": "ASR.BlockExesMail parameter" }, "blockJSVB": { - "type": "boolean" + "type": "boolean", + "description": "ASR.blockJSVB parameter" }, "BlockObfuscatedScripts": { - "type": "boolean" + "type": "boolean", + "description": "ASR.BlockObfuscatedScripts parameter" }, "BlockOfficeApps": { - "type": "boolean" + "type": "boolean", + "description": "ASR.BlockOfficeApps parameter" }, "blockOfficeChild": { - "type": "boolean" + "type": "boolean", + "description": "ASR.blockOfficeChild parameter" }, "blockOfficeComChild": { - "type": "boolean" + "type": "boolean", + "description": "ASR.blockOfficeComChild parameter" }, "BlockOfficeExes": { - "type": "boolean" + "type": "boolean", + "description": "ASR.BlockOfficeExes parameter" }, "BlockPSExec": { - "type": "boolean" + "type": "boolean", + "description": "ASR.BlockPSExec parameter" }, "BlockSafeMode": { - "type": "boolean" + "type": "boolean", + "description": "ASR.BlockSafeMode parameter" }, "BlockSystemTools": { - "type": "boolean" + "type": "boolean", + "description": "ASR.BlockSystemTools parameter" }, "BlockUnsignedDrivers": { - "type": "boolean" + "type": "boolean", + "description": "ASR.BlockUnsignedDrivers parameter" }, "BlockUntrustedUSB": { - "type": "boolean" + "type": "boolean", + "description": "ASR.BlockUntrustedUSB parameter" }, "BlockWebshellForServers": { - "type": "boolean" + "type": "boolean", + "description": "ASR.BlockWebshellForServers parameter" }, "BlockWin32Macro": { - "type": "boolean" + "type": "boolean", + "description": "ASR.BlockWin32Macro parameter" }, "BlockYoungExe": { - "type": "boolean" + "type": "boolean", + "description": "ASR.BlockYoungExe parameter" }, "EnableRansomwareVac": { - "type": "boolean" + "type": "boolean", + "description": "ASR.EnableRansomwareVac parameter" }, "Mode": { "type": "string", @@ -1302,15 +1715,18 @@ "block", "audit", "warn" - ] + ], + "description": "ASR.Mode parameter" }, "WMIPersistence": { - "type": "boolean" + "type": "boolean", + "description": "ASR.WMIPersistence parameter" } } }, "EDR": { "type": "object", + "description": "EDR parameter", "properties": { "AssignTo": { "type": "string", @@ -1319,18 +1735,22 @@ "allLicensedUsers", "AllDevices", "AllDevicesAndUsers" - ] + ], + "description": "EDR.AssignTo parameter" }, "Config": { - "type": "boolean" + "type": "boolean", + "description": "EDR.Config parameter" }, "SampleSharing": { - "type": "boolean" + "type": "boolean", + "description": "EDR.SampleSharing parameter" } } }, "Exclusion": { "type": "object", + "description": "Exclusion parameter", "properties": { "AssignTo": { "type": "string", @@ -1339,45 +1759,62 @@ "allLicensedUsers", "AllDevices", "AllDevicesAndUsers" - ] + ], + "description": "Exclusion.AssignTo parameter" } } }, "Policy": { "type": "object", + "description": "Policy parameter", "properties": { "AllowBehavior": { - "type": "boolean" + "type": "boolean", + "description": "Policy.AllowBehavior parameter" }, "AllowCloudProtection": { - "type": "boolean" + "type": "boolean", + "description": "Policy.AllowCloudProtection parameter" }, "AllowDownloadable": { - "type": "boolean" + "type": "boolean", + "description": "Policy.AllowDownloadable parameter" }, "AllowEmailScanning": { - "type": "boolean" + "type": "boolean", + "description": "Policy.AllowEmailScanning parameter" }, "AllowFullScanNetwork": { - "type": "boolean" + "type": "boolean", + "description": "Policy.AllowFullScanNetwork parameter" }, "AllowFullScanRemovable": { - "type": "boolean" + "type": "boolean", + "description": "Policy.AllowFullScanRemovable parameter" }, "AllowNetwork": { - "type": "boolean" + "type": "boolean", + "description": "Policy.AllowNetwork parameter" }, "AllowOnAccessProtection": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "Policy.AllowOnAccessProtection parameter" }, "AllowRealTime": { - "type": "boolean" + "type": "boolean", + "description": "Policy.AllowRealTime parameter" }, "AllowScriptScan": { - "type": "boolean" + "type": "boolean", + "description": "Policy.AllowScriptScan parameter" }, "AllowUI": { - "type": "boolean" + "type": "boolean", + "description": "Policy.AllowUI parameter" }, "AssignTo": { "type": "string", @@ -1386,113 +1823,245 @@ "allLicensedUsers", "AllDevices", "AllDevicesAndUsers" - ] + ], + "description": "Policy.AssignTo parameter" }, "AvgCPULoadFactor": { - "type": "number" + "type": "number", + "description": "Policy.AvgCPULoadFactor parameter" }, "CheckSigs": { - "type": "boolean" + "type": "boolean", + "description": "Policy.CheckSigs parameter" }, "CloudBlockLevel": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "Policy.CloudBlockLevel parameter" }, "CloudExtendedTimeout": { - "type": "number" + "type": "number", + "description": "Policy.CloudExtendedTimeout parameter" }, "DisableCatchupFullScan": { - "type": "boolean" + "type": "boolean", + "description": "Policy.DisableCatchupFullScan parameter" }, "DisableCatchupQuickScan": { - "type": "boolean" + "type": "boolean", + "description": "Policy.DisableCatchupQuickScan parameter" }, "DisableLocalAdminMerge": { - "type": "boolean" + "type": "boolean", + "description": "Policy.DisableLocalAdminMerge parameter" }, "EnableNetworkProtection": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "Policy.EnableNetworkProtection parameter" }, "LowCPU": { - "type": "boolean" + "type": "boolean", + "description": "Policy.LowCPU parameter" }, "MeteredConnectionUpdates": { - "type": "boolean" + "type": "boolean", + "description": "Policy.MeteredConnectionUpdates parameter" }, "Remediation.High": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "Policy.Remediation.High parameter" }, "Remediation.Low": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "Policy.Remediation.Low parameter" }, "Remediation.Moderate": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "Policy.Remediation.Moderate parameter" }, "Remediation.Severe": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "Policy.Remediation.Severe parameter" }, "ScanArchives": { - "type": "boolean" + "type": "boolean", + "description": "Policy.ScanArchives parameter" }, "SignatureUpdateInterval": { - "type": "number" + "type": "number", + "description": "Policy.SignatureUpdateInterval parameter" }, "SubmitSamplesConsent": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "Policy.SubmitSamplesConsent parameter" } } }, "selectedTenants": { - "type": "string" + "type": "string", + "description": "selectedTenants parameter" }, "showASR": { - "type": "boolean" + "type": "boolean", + "description": "showASR parameter" }, "showDefenderDefaults": { - "type": "boolean" + "type": "boolean", + "description": "showDefenderDefaults parameter" }, "showDefenderSetup": { - "type": "boolean" + "type": "boolean", + "description": "showDefenderSetup parameter" }, "showExclusionPolicy": { - "type": "boolean" + "type": "boolean", + "description": "showExclusionPolicy parameter" }, "appSync": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: appSync" }, "ConnectIos": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: ConnectIos" }, "ConnectAndroid": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: ConnectAndroid" }, "ConnectIosCompliance": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: ConnectIosCompliance" }, "BlockunsupportedOS": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: BlockunsupportedOS" }, "ConnectMac": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: ConnectMac" }, "Connectwindows": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: Connectwindows" }, "ConnectAndroidCompliance": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: ConnectAndroidCompliance" }, "AssignTo": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: AssignTo" }, "excludedExtensions": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: excludedExtensions" }, "excludedProcesses": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: excludedProcesses" }, "excludedPaths": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: excludedPaths" }, "Mode": { + "type": "string", + "description": "Body parameter from PS1: Mode" + } + } + } + } + } + } + } + }, + "/api/AddDefenderTemplate": { + "post": { + "summary": "AddDefenderTemplate", + "tags": [ + "Endpoint > MEM" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.MEM.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ASR": { + "type": "string" + }, + "EDR": { + "type": "string" + }, + "Exclusion": { + "type": "string" + }, + "package": { + "type": "string" + }, + "Policy": { + "type": "string" + }, + "templateName": { "type": "string" } } @@ -1502,11 +2071,11 @@ } } }, - "/api/AddDomain": { + "/api/AddDlpCompliancePolicy": { "post": { - "summary": "AddDomain", + "summary": "AddDlpCompliancePolicy", "tags": [ - "Tenant > Administration > Domains" + "Security > Compliance-DLP" ], "security": [ { @@ -1525,13 +2094,122 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Security.DlpCompliancePolicy.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "PowerShellCommand": { + "type": "string" + }, + "selectedTenants": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/AddDlpCompliancePolicyTemplate": { + "post": { + "summary": "AddDlpCompliancePolicyTemplate", + "tags": [ + "Security > Compliance-DLP" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Security.DlpCompliancePolicy.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "PowerShellCommand": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/AddDomain": { + "post": { + "summary": "AddDomain", + "tags": [ + "Tenant > Administration > Domains" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -1563,7 +2241,7 @@ }, "/api/AddEditTransportRule": { "post": { - "summary": "AddEditTransportRule", + "summary": "This function creates a new transport rule or edits an existing one (mail flow rule).", "tags": [ "Email-Exchange > Transport" ], @@ -1584,19 +2262,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "description": "This function creates a new transport rule or edits an existing one (mail flow rule).\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", + "description": "This function creates a new transport rule or edits an existing one (mail flow rule).", "x-cipp-role": "Exchange.TransportRule.ReadWrite", "requestBody": { "required": true, @@ -1651,357 +2329,492 @@ "type": "array", "items": { "type": "string" - } + }, + "description": "actionType parameter" }, "ActivationDate": { "type": "string", - "format": "date-time" + "format": "date-time", + "description": "ActivationDate parameter" }, "AnyOfCcHeader": { - "type": "string" + "type": "string", + "description": "AnyOfCcHeader parameter" }, "AnyOfCcHeaderMemberOf": { - "type": "string" + "type": "string", + "description": "AnyOfCcHeaderMemberOf parameter" }, "AnyOfRecipientAddressContainsWords": { - "type": "string" + "type": "string", + "description": "AnyOfRecipientAddressContainsWords parameter" }, "AnyOfRecipientAddressMatchesPatterns": { - "type": "string" + "type": "string", + "description": "AnyOfRecipientAddressMatchesPatterns parameter" }, "AnyOfToCcHeader": { - "type": "string" + "type": "string", + "description": "AnyOfToCcHeader parameter" }, "AnyOfToCcHeaderMemberOf": { - "type": "string" + "type": "string", + "description": "AnyOfToCcHeaderMemberOf parameter" }, "AnyOfToHeader": { - "type": "string" + "type": "string", + "description": "AnyOfToHeader parameter" }, "AnyOfToHeaderMemberOf": { - "type": "string" + "type": "string", + "description": "AnyOfToHeaderMemberOf parameter" }, "ApplyClassification": { - "type": "string" + "type": "string", + "description": "ApplyClassification parameter" }, "ApplyHtmlDisclaimerFallbackAction": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "ApplyHtmlDisclaimerFallbackAction parameter" }, "ApplyHtmlDisclaimerLocation": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "ApplyHtmlDisclaimerLocation parameter" }, "ApplyHtmlDisclaimerText": { - "type": "string" + "type": "string", + "description": "ApplyHtmlDisclaimerText parameter" }, "ApplyOME": { - "type": "string" + "type": "string", + "description": "ApplyOME parameter" }, "applyToAllMessages": { - "type": "boolean" + "type": "boolean", + "description": "applyToAllMessages parameter" }, "AttachmentContainsWords": { - "type": "string" + "type": "string", + "description": "AttachmentContainsWords parameter" }, "AttachmentExtensionMatchesWords": { - "type": "string" + "type": "string", + "description": "AttachmentExtensionMatchesWords parameter" }, "AttachmentMatchesPatterns": { - "type": "string" + "type": "string", + "description": "AttachmentMatchesPatterns parameter" }, "AttachmentSizeOver": { - "type": "string" + "type": "string", + "description": "AttachmentSizeOver parameter" }, "BlindCopyTo": { - "type": "string" + "type": "string", + "description": "BlindCopyTo parameter" }, "Comments": { - "type": "string" + "type": "string", + "description": "Comments parameter" }, "conditionType": { "type": "array", "items": { "type": "string" - } + }, + "description": "conditionType parameter" }, "CopyTo": { - "type": "string" + "type": "string", + "description": "CopyTo parameter" }, "DeleteMessage": { - "type": "string" + "type": "string", + "description": "DeleteMessage parameter" }, "Enabled": { - "type": "boolean" + "type": "boolean", + "description": "Enabled parameter" }, "ExceptIfAnyOfCcHeader": { - "type": "string" + "type": "string", + "description": "ExceptIfAnyOfCcHeader parameter" }, "ExceptIfAnyOfCcHeaderMemberOf": { - "type": "string" + "type": "string", + "description": "ExceptIfAnyOfCcHeaderMemberOf parameter" }, "ExceptIfAnyOfRecipientAddressContainsWords": { - "type": "string" + "type": "string", + "description": "ExceptIfAnyOfRecipientAddressContainsWords parameter" }, "ExceptIfAnyOfRecipientAddressMatchesPatterns": { - "type": "string" + "type": "string", + "description": "ExceptIfAnyOfRecipientAddressMatchesPatterns parameter" }, "ExceptIfAnyOfToCcHeader": { - "type": "string" + "type": "string", + "description": "ExceptIfAnyOfToCcHeader parameter" }, "ExceptIfAnyOfToCcHeaderMemberOf": { - "type": "string" + "type": "string", + "description": "ExceptIfAnyOfToCcHeaderMemberOf parameter" }, "ExceptIfAnyOfToHeader": { - "type": "string" + "type": "string", + "description": "ExceptIfAnyOfToHeader parameter" }, "ExceptIfAnyOfToHeaderMemberOf": { - "type": "string" + "type": "string", + "description": "ExceptIfAnyOfToHeaderMemberOf parameter" }, "ExceptIfAttachmentContainsWords": { - "type": "string" + "type": "string", + "description": "ExceptIfAttachmentContainsWords parameter" }, "ExceptIfAttachmentExtensionMatchesWords": { - "type": "string" + "type": "string", + "description": "ExceptIfAttachmentExtensionMatchesWords parameter" }, "ExceptIfAttachmentMatchesPatterns": { - "type": "string" + "type": "string", + "description": "ExceptIfAttachmentMatchesPatterns parameter" }, "ExceptIfAttachmentSizeOver": { - "type": "string" + "type": "string", + "description": "ExceptIfAttachmentSizeOver parameter" }, "ExceptIfFrom": { - "type": "string" + "type": "string", + "description": "ExceptIfFrom parameter" }, "ExceptIfFromAddressContainsWords": { - "type": "string" + "type": "string", + "description": "ExceptIfFromAddressContainsWords parameter" }, "ExceptIfFromAddressMatchesPatterns": { - "type": "string" + "type": "string", + "description": "ExceptIfFromAddressMatchesPatterns parameter" }, "ExceptIfFromMemberOf": { - "type": "string" + "type": "string", + "description": "ExceptIfFromMemberOf parameter" }, "ExceptIfFromScope": { - "type": "string" + "type": "string", + "description": "ExceptIfFromScope parameter" }, "ExceptIfHeaderContainsWords": { - "type": "string" + "type": "string", + "description": "ExceptIfHeaderContainsWords parameter" }, "ExceptIfHeaderContainsWordsMessageHeader": { - "type": "string" + "type": "string", + "description": "ExceptIfHeaderContainsWordsMessageHeader parameter" }, "ExceptIfHeaderMatchesPatterns": { - "type": "string" + "type": "string", + "description": "ExceptIfHeaderMatchesPatterns parameter" }, "ExceptIfHeaderMatchesPatternsMessageHeader": { - "type": "string" + "type": "string", + "description": "ExceptIfHeaderMatchesPatternsMessageHeader parameter" }, "ExceptIfMessageSizeOver": { - "type": "string" + "type": "string", + "description": "ExceptIfMessageSizeOver parameter" }, "ExceptIfMessageTypeMatches": { - "type": "string" + "type": "string", + "description": "ExceptIfMessageTypeMatches parameter" }, "ExceptIfRecipientAddressContainsWords": { - "type": "string" + "type": "string", + "description": "ExceptIfRecipientAddressContainsWords parameter" }, "ExceptIfRecipientAddressMatchesPatterns": { - "type": "string" + "type": "string", + "description": "ExceptIfRecipientAddressMatchesPatterns parameter" }, "ExceptIfRecipientDomainIs": { - "type": "string" + "type": "string", + "description": "ExceptIfRecipientDomainIs parameter" }, "ExceptIfSCLOver": { - "type": "string" + "type": "string", + "description": "ExceptIfSCLOver parameter" }, "ExceptIfSenderDomainIs": { - "type": "string" + "type": "string", + "description": "ExceptIfSenderDomainIs parameter" }, "ExceptIfSenderIpRanges": { - "type": "string" + "type": "string", + "description": "ExceptIfSenderIpRanges parameter" }, "ExceptIfSentTo": { - "type": "string" + "type": "string", + "description": "ExceptIfSentTo parameter" }, "ExceptIfSentToMemberOf": { - "type": "string" + "type": "string", + "description": "ExceptIfSentToMemberOf parameter" }, "ExceptIfSentToScope": { - "type": "string" + "type": "string", + "description": "ExceptIfSentToScope parameter" }, "ExceptIfSubjectContainsWords": { - "type": "string" + "type": "string", + "description": "ExceptIfSubjectContainsWords parameter" }, "ExceptIfSubjectMatchesPatterns": { - "type": "string" + "type": "string", + "description": "ExceptIfSubjectMatchesPatterns parameter" }, "ExceptIfSubjectOrBodyContainsWords": { - "type": "string" + "type": "string", + "description": "ExceptIfSubjectOrBodyContainsWords parameter" }, "ExceptIfSubjectOrBodyMatchesPatterns": { - "type": "string" + "type": "string", + "description": "ExceptIfSubjectOrBodyMatchesPatterns parameter" }, "ExceptIfWithImportance": { - "type": "string" + "type": "string", + "description": "ExceptIfWithImportance parameter" }, "exceptionType": { "type": "array", "items": { "type": "string" - } + }, + "description": "exceptionType parameter" }, "ExpiryDate": { "type": "string", - "format": "date-time" + "format": "date-time", + "description": "ExpiryDate parameter" }, "From": { - "type": "string" + "type": "string", + "description": "From parameter" }, "FromAddressContainsWords": { - "type": "string" + "type": "string", + "description": "FromAddressContainsWords parameter" }, "FromAddressMatchesPatterns": { - "type": "string" + "type": "string", + "description": "FromAddressMatchesPatterns parameter" }, "FromMemberOf": { - "type": "string" + "type": "string", + "description": "FromMemberOf parameter" }, "FromScope": { - "type": "string" + "type": "string", + "description": "FromScope parameter" }, "GenerateIncidentReport": { - "type": "string" + "type": "string", + "description": "GenerateIncidentReport parameter" }, "GenerateNotification": { - "type": "string" + "type": "string", + "description": "GenerateNotification parameter" }, "HeaderContainsWords": { - "type": "string" + "type": "string", + "description": "HeaderContainsWords parameter" }, "HeaderContainsWordsMessageHeader": { - "type": "string" + "type": "string", + "description": "HeaderContainsWordsMessageHeader parameter" }, "HeaderMatchesPatterns": { - "type": "string" + "type": "string", + "description": "HeaderMatchesPatterns parameter" }, "HeaderMatchesPatternsMessageHeader": { - "type": "string" + "type": "string", + "description": "HeaderMatchesPatternsMessageHeader parameter" }, "MessageSizeOver": { - "type": "string" + "type": "string", + "description": "MessageSizeOver parameter" }, "MessageTypeMatches": { - "type": "string" + "type": "string", + "description": "MessageTypeMatches parameter" }, "Mode": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "Mode parameter" }, "ModerateMessageByManager": { - "type": "string" + "type": "string", + "description": "ModerateMessageByManager parameter" }, "ModerateMessageByUser": { - "type": "string" + "type": "string", + "description": "ModerateMessageByUser parameter" }, "Name": { - "type": "string" + "type": "string", + "description": "Name parameter" }, "PrependSubject": { - "type": "string" + "type": "string", + "description": "PrependSubject parameter" }, "Priority": { - "type": "number" + "type": "number", + "description": "Priority parameter" }, "Quarantine": { - "type": "string" + "type": "string", + "description": "Quarantine parameter" }, "RecipientAddressContainsWords": { - "type": "string" + "type": "string", + "description": "RecipientAddressContainsWords parameter" }, "RecipientAddressMatchesPatterns": { - "type": "string" + "type": "string", + "description": "RecipientAddressMatchesPatterns parameter" }, "RecipientDomainIs": { - "type": "string" + "type": "string", + "description": "RecipientDomainIs parameter" }, "RedirectMessageTo": { - "type": "string" + "type": "string", + "description": "RedirectMessageTo parameter" }, "RejectMessageEnhancedStatusCode": { - "type": "string" + "type": "string", + "description": "RejectMessageEnhancedStatusCode parameter" }, "RejectMessageReasonText": { - "type": "string" + "type": "string", + "description": "RejectMessageReasonText parameter" }, "RemoveHeader": { - "type": "string" + "type": "string", + "description": "RemoveHeader parameter" }, "RouteMessageOutboundConnector": { - "type": "string" + "type": "string", + "description": "RouteMessageOutboundConnector parameter" }, "ruleId": { - "type": "string" + "type": "string", + "description": "ruleId parameter" }, "SCLOver": { - "type": "string" + "type": "string", + "description": "SCLOver parameter" }, "SenderAddressLocation": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "SenderAddressLocation parameter" }, "SenderDomainIs": { - "type": "string" + "type": "string", + "description": "SenderDomainIs parameter" }, "SenderIpRanges": { - "type": "string" + "type": "string", + "description": "SenderIpRanges parameter" }, "SentTo": { - "type": "string" + "type": "string", + "description": "SentTo parameter" }, "SentToMemberOf": { - "type": "string" + "type": "string", + "description": "SentToMemberOf parameter" }, "SentToScope": { - "type": "string" + "type": "string", + "description": "SentToScope parameter" }, "SetAuditSeverity": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "SetAuditSeverity parameter" }, "SetHeaderName": { - "type": "string" + "type": "string", + "description": "SetHeaderName parameter" }, "SetHeaderValue": { - "type": "string" + "type": "string", + "description": "SetHeaderValue parameter" }, "SetSCL": { - "type": "string" + "type": "string", + "description": "SetSCL parameter" }, "State": { - "type": "string" + "type": "string", + "description": "State parameter" }, "StopRuleProcessing": { - "type": "boolean" + "type": "boolean", + "description": "StopRuleProcessing parameter" }, "SubjectContainsWords": { - "type": "string" + "type": "string", + "description": "SubjectContainsWords parameter" }, "SubjectMatchesPatterns": { - "type": "string" + "type": "string", + "description": "SubjectMatchesPatterns parameter" }, "SubjectOrBodyContainsWords": { - "type": "string" + "type": "string", + "description": "SubjectOrBodyContainsWords parameter" }, "SubjectOrBodyMatchesPatterns": { - "type": "string" + "type": "string", + "description": "SubjectOrBodyMatchesPatterns parameter" }, "tenantFilter": { - "type": "string" + "type": "string", + "description": "tenantFilter parameter" }, "WithImportance": { - "type": "string" + "type": "string", + "description": "WithImportance parameter" }, "value": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: value" }, "Count": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: Count" } }, "required": [ @@ -2036,13 +2849,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -2116,13 +2929,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -2137,19 +2950,28 @@ "type": "object", "properties": { "displayName": { - "type": "string" + "type": "string", + "description": "displayName parameter" }, "domain": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "domain parameter" }, "tenantID": { - "type": "string" + "type": "string", + "description": "tenantID parameter" }, "username": { - "type": "string" + "type": "string", + "description": "username parameter" }, "userPrincipalName": { - "type": "string" + "type": "string", + "description": "userPrincipalName parameter" } } } @@ -2181,13 +3003,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -2202,16 +3024,24 @@ "type": "object", "properties": { "PowerShellCommand": { - "type": "string" + "type": "string", + "description": "PowerShellCommand parameter" }, "selectedTenants": { - "type": "string" + "type": "string", + "description": "selectedTenants parameter" }, "TemplateList": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "TemplateList parameter" }, "comment": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: comment" } } } @@ -2243,13 +3073,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -2299,13 +3129,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -2320,16 +3150,20 @@ "type": "object", "properties": { "membershipRules": { - "type": "string" + "type": "string", + "description": "(auto) from CippAddGroupForm.jsx" }, "displayName": { - "type": "string" + "type": "string", + "description": "(auto) from CippAddGroupForm.jsx" }, "description": { - "type": "string" + "type": "string", + "description": "(auto) from CippAddGroupForm.jsx" }, "username": { - "type": "string" + "type": "string", + "description": "(auto) from CippAddGroupForm.jsx" }, "groupType": { "type": "string", @@ -2341,31 +3175,42 @@ "dynamicdistribution", "distribution", "security" - ] + ], + "description": "(auto) from CippAddGroupForm.jsx" }, "allowExternal": { - "type": "boolean" + "type": "boolean", + "description": "(auto) from CippAddGroupForm.jsx" }, "subscribeMembers": { - "type": "boolean" + "type": "boolean", + "description": "(auto) from CippAddGroupForm.jsx" }, "primDomain": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "(auto) from CippAddGroupForm.jsx" }, "owners": { "type": "array", "items": { "type": "string" - } + }, + "description": "(auto) from CippAddGroupForm.jsx (array of LabelValue)" }, "members": { "type": "array", "items": { "type": "string" - } + }, + "description": "(auto) from CippAddGroupForm.jsx (array of LabelValue)" }, "tenantFilter": { - "type": "string" + "type": "string", + "description": "(auto) from API scan" } }, "required": [ @@ -2400,13 +3245,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -2462,13 +3307,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -2482,11 +3327,20 @@ "schema": { "type": "object", "properties": { + "licenses": { + "type": "array", + "items": { + "type": "string" + }, + "description": "License SKU IDs to assign" + }, "GUID": { - "type": "string" + "type": "string", + "description": "(auto) from CippAddGroupTemplateForm.jsx" }, "username": { - "type": "string" + "type": "string", + "description": "(auto) from CippAddGroupTemplateForm.jsx" }, "groupType": { "type": "string", @@ -2498,22 +3352,28 @@ "dynamicDistribution", "distribution", "security" - ] + ], + "description": "(auto) from CippAddGroupTemplateForm.jsx" }, "allowExternal": { - "type": "boolean" + "type": "boolean", + "description": "(auto) from CippAddGroupTemplateForm.jsx" }, "subscribeMembers": { - "type": "boolean" + "type": "boolean", + "description": "(auto) from CippAddGroupTemplateForm.jsx" }, "membershipRules": { - "type": "string" + "type": "string", + "description": "(auto) from CippAddGroupTemplateForm.jsx" }, "displayname": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: displayname" }, "Description": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: Description" } } } @@ -2545,13 +3405,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -2569,36 +3429,46 @@ "type": "object", "properties": { "displayName": { - "type": "string" + "type": "string", + "description": "addrow.displayName parameter" }, "mail": { - "type": "string" + "type": "string", + "description": "addrow.mail parameter" }, "redirectUri": { - "type": "string" + "type": "string", + "description": "addrow.redirectUri parameter" } } }, "bulkGuests": { - "type": "string" + "type": "string", + "description": "bulkGuests parameter" }, "displayName": { - "type": "string" + "type": "string", + "description": "displayName parameter" }, "mail": { - "type": "string" + "type": "string", + "description": "mail parameter" }, "message": { - "type": "string" + "type": "string", + "description": "message parameter" }, "redirectUri": { - "type": "string" + "type": "string", + "description": "redirectUri parameter" }, "sendInvite": { - "type": "boolean" + "type": "boolean", + "description": "sendInvite parameter" }, "tenantFilter": { - "type": "string" + "type": "string", + "description": "tenantFilter parameter" } }, "required": [ @@ -2633,13 +3503,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -2655,7 +3525,8 @@ "in": "query", "required": false, "schema": { - "type": "string" + "type": "string", + "description": "Query parameter from PS1: TemplateId" } } ], @@ -2667,25 +3538,31 @@ "type": "object", "properties": { "tenantFilter": { - "type": "string" + "type": "string", + "description": "Target tenant. PS1 also checks $Request.Query.tenantFilter." }, "ID": { - "type": "string" + "type": "string", + "description": "ID of the IntuneReusableSettingTemplate to deploy; sent as 'TemplateId' in older path. PS1 reads $Request.Body.TemplateId ?? $Request.Body.TemplateList?.value ?? $Request.Body.TemplateList. The edit form sends 'ID' and 'TemplateId' both set to the same value." }, "TemplateId": { - "type": "string" + "type": "string", + "description": "Legacy alias for ID; PS1 resolves TemplateId first then falls back to ID via TemplateList." }, "displayName": { - "type": "string" + "type": "string", + "description": "Display name to override; propagated into rawJSON by formatter." }, "description": { "type": "string" }, "rawJSON": { - "type": "string" + "type": "string", + "description": "Serialised JSON string of the full reusable-policy-setting Graph object. PS1 reads this from the template entity, not directly from the request body \u2014 the body only supplies TemplateId." }, "TemplateList": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: TemplateList" } }, "required": [ @@ -2720,13 +3597,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -2741,37 +3618,48 @@ "type": "object", "properties": { "GUID": { - "type": "string" + "type": "string", + "description": "Template GUID; auto-generated by PS1 if not supplied. Both Add (new) and Edit forms send this." }, "displayName": { - "type": "string" + "type": "string", + "description": "PS1 reads $Request.Body.displayName ?? $Request.Body.DisplayName ?? $Request.Body.displayname" }, "description": { - "type": "string" + "type": "string", + "description": "PS1 reads $Request.Body.description ?? $Request.Body.Description" }, "rawJSON": { - "type": "string" + "type": "string", + "description": "Serialised JSON string of the full reusable-policy-setting graph object. PS1 reads $Request.Body.rawJSON ?? $Request.Body.RawJSON ?? $Request.Body.json" }, "tenantFilter": { - "type": "string" + "type": "string", + "description": "Used by the edit form; the PS1 for AddIntuneReusableSettingTemplate is AnyTenant so tenantFilter is informational only here." }, "package": { - "type": "string" + "type": "string", + "description": "Present in the edit-form formatter output but not read by the PS1; likely a passthrough for future use." }, "DisplayName": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: DisplayName" }, "displayname": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: displayname" }, "Description": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: Description" }, "RawJSON": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: RawJSON" }, "json": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: json" } }, "required": [ @@ -2806,13 +3694,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -2915,13 +3803,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -2935,53 +3823,114 @@ "schema": { "type": "object", "properties": { + "defaultUseGroups": { + "type": "boolean" + }, + "defaultUseRoles": { + "type": "boolean" + }, "defaultDomain": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "Pre-selects a domain when userAction is \"create\". LabelValue shape: { \"label\": \"contoso.com\", \"value\": \"contoso.com\" }. The value is the domain string. Accepts a plain string." }, "defaultDuration": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "Default JIT access duration. LabelValue shape: { \"label\": \"4 Hours\", \"value\": \"PT4H\" }. The value must be an ISO 8601 duration string. Common presets: \"PT1H\" (1 hr), \"PT4H\" (4 hrs), \"PT8H\" (8 hrs), \"P1D\" (1 day), \"P3D\", \"P7D\", \"P14D\", \"P30D\". Custom durations like \"PT2H30M\" are also valid. Cannot exceed the global MaxDuration set in JIT Admin Settings." }, "defaultExistingUser": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "Pre-selects a user when defaultUserAction is \"select\". LabelValue shape: { \"label\": \"User Display Name\", \"value\": \"\" }. The PS1 accepts either a UPN or an ObjectId GUID as the value." }, "defaultExpireAction": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "Default expiration action for JIT sessions using this template. LabelValue shape: { \"label\": \"...\", \"value\": \"\" }. Valid values: \"DisableUser\", \"DeleteUser\", \"RemoveRoles\" (select only), \"RemoveGroups\" (select only), \"RemoveRolesAndGroups\" (select only). The \"Remove*\" variants require defaultUserAction=\"select\"." }, "defaultFirstName": { - "type": "string" + "type": "string", + "description": "Pre-fills the first name field when defaultUserAction is \"create\"." }, "defaultForTenant": { - "type": "boolean" + "type": "boolean", + "description": "If true, marks this template as the default for the target tenant. Only one template can be the tenant default." + }, + "defaultGroups": { + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "Array of groups pre-selected in the template. LabelValue shape: { \"label\": \"Group Display Name\", \"value\": \"\" }. The value must be the group's ObjectId (GUID). Stored as-is and applied when a JIT session is created from this template." }, "defaultLastName": { - "type": "string" + "type": "string", + "description": "Pre-fills the last name field when defaultUserAction is \"create\"." }, "defaultNotificationActions": { "type": "array", "items": { "type": "string" - } + }, + "description": "Default notification actions for JIT sessions created from this template. Array of strings. Valid values: \"Webhook\", \"email\", \"PSA\"." }, "defaultRoles": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "Array of admin roles pre-selected in the template. LabelValue shape: { \"label\": \"Role Display Name\", \"value\": \"\" }. The value must be the role's ObjectId (GUID). Retrieve available roles from GET /api/ListRoles?TenantFilter=. Stored as-is and applied when a JIT session is created from this template." }, "defaultUserAction": { - "type": "string" + "type": "string", + "enum": [ + "create", + "select" + ], + "description": "Whether sessions created from this template target a new or existing user. Valid values: \"create\" (new user), \"select\" (existing user). When set to \"AllTenants\", only \"create\" is permitted." + }, + "defaultUsageLocation": { + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "Default usage location for new users created from this template. LabelValue shape: { \"label\": \"Canada\", \"value\": \"CA\" }. The value must be an ISO 3166-1 alpha-2 country code. Accepts a plain country code string. PS1 unwraps .value with fallback to the raw string." }, "defaultUserName": { - "type": "string" + "type": "string", + "description": "Pre-fills the username prefix field when defaultUserAction is \"create\"." }, "generateTAPByDefault": { - "type": "boolean" + "type": "boolean", + "description": "If true, JIT sessions created from this template will generate a Temporary Access Pass instead of a password. Only applies when defaultUserAction=\"create\"." }, "reasonTemplate": { - "type": "string" + "type": "string", + "description": "Default reason/justification text pre-filled when creating a JIT session from this template." }, "templateName": { - "type": "string" + "type": "string", + "description": "Unique name for this template within the target tenant. Required." }, "tenantFilter": { - "type": "string" + "type": "string", + "description": "Target tenant domain or GUID, or \"AllTenants\" to create a global template. \"AllTenants\" templates cannot use defaultUserAction=\"select\"." } }, "required": [ @@ -3016,13 +3965,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -3037,31 +3986,43 @@ "type": "object", "properties": { "AssignTo": { - "type": "string" + "type": "string", + "description": "AssignTo parameter" }, "CustomGroup": { - "type": "string" + "type": "string", + "description": "CustomGroup parameter" }, "DisplayName": { - "type": "string" + "type": "string", + "description": "DisplayName parameter" }, "PackageName": { - "type": "string" + "type": "string", + "description": "PackageName parameter" }, "params": { - "type": "string" + "type": "string", + "description": "params parameter" }, "RMMName": { - "$ref": "#/components/schemas/LabelValue", + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "RMMName parameter", "type": "object", "properties": { "value": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: RMMName.value" } } }, "selectedTenants": { - "type": "string" + "type": "string", + "description": "selectedTenants parameter" } } } @@ -3093,13 +4054,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -3171,13 +4132,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -3209,6 +4170,12 @@ "excludedApps": { "type": "string" }, + "excludeGroup": { + "type": "string" + }, + "IntuneBody": { + "type": "string" + }, "languages": { "type": "string" }, @@ -3257,13 +4224,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -3281,49 +4248,64 @@ "type": "string" }, "displayName": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: displayName" }, "Description": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: Description" }, "AssignTo": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: AssignTo" }, "excludeGroup": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: excludeGroup" }, "AssignmentFilterName": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: AssignmentFilterName" }, "assignmentFilter": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: assignmentFilter" }, "assignmentFilterType": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: assignmentFilterType" }, "customGroup": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: customGroup" }, "RAWJson": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: RAWJson" }, "replacemap": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: replacemap" }, "reusableSettings": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: reusableSettings" }, "TemplateID": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: TemplateID" }, "TemplateGUID": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: TemplateGUID" }, "TemplateType": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: TemplateType" }, "Count": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: Count" } }, "required": [ @@ -3358,13 +4340,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -3379,34 +4361,52 @@ "type": "object", "properties": { "AllowSender": { - "type": "boolean" + "type": "boolean", + "description": "AllowSender parameter" }, "BlockSender": { - "type": "boolean" + "type": "boolean", + "description": "BlockSender parameter" }, "Delete": { - "type": "boolean" + "type": "boolean", + "description": "Delete parameter" }, "IncludeMessagesFromBlockedSenderAddress": { - "type": "boolean" + "type": "boolean", + "description": "IncludeMessagesFromBlockedSenderAddress parameter" }, "Name": { - "type": "string" + "type": "string", + "description": "Name parameter" }, "Preview": { - "type": "boolean" + "type": "boolean", + "description": "Preview parameter" }, "QuarantineNotification": { - "type": "boolean" + "type": "boolean", + "description": "QuarantineNotification parameter" }, "ReleaseActionPreference": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "ReleaseActionPreference parameter" }, "selectedTenants": { - "type": "string" + "type": "string", + "description": "selectedTenants parameter" }, "TemplateList": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "TemplateList parameter" } } } @@ -3415,11 +4415,67 @@ } } }, - "/api/AddRoomList": { + "/api/AddRetentionCompliancePolicy": { "post": { - "summary": "AddRoomList", + "summary": "AddRetentionCompliancePolicy", "tags": [ - "Email-Exchange > Resources" + "Security > Compliance-Retention" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Security.RetentionCompliancePolicy.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "PowerShellCommand": { + "type": "string" + }, + "selectedTenants": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/AddRetentionCompliancePolicyTemplate": { + "post": { + "summary": "AddRetentionCompliancePolicyTemplate", + "tags": [ + "Security > Compliance-Retention" ], "security": [ { @@ -3438,13 +4494,83 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Security.RetentionCompliancePolicy.ReadWrite", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Identity": { + "type": "string" + }, + "name": { + "type": "string" + }, + "PowerShellCommand": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/AddRoomList": { + "post": { + "summary": "AddRoomList", + "tags": [ + "Email-Exchange > Resources" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -3506,13 +4632,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -3527,22 +4653,32 @@ "type": "object", "properties": { "domain": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "domain parameter" }, "ResourceCapacity": { - "type": "string" + "type": "string", + "description": "ResourceCapacity parameter" }, "tenantid": { - "type": "string" + "type": "string", + "description": "tenantid parameter" }, "username": { - "type": "string" + "type": "string", + "description": "username parameter" }, "userPrincipalName": { - "type": "string" + "type": "string", + "description": "userPrincipalName parameter" }, "DisplayName": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: DisplayName" } } } @@ -3553,7 +4689,7 @@ }, "/api/AddSafeLinksPolicyFromTemplate": { "post": { - "summary": "AddSafeLinksPolicyFromTemplate", + "summary": "This function deploys SafeLinks policies and rules from templates to selected tenants.", "tags": [ "Security > Safe-Links-Policy" ], @@ -3574,19 +4710,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "description": "This function deploys SafeLinks policies and rules from templates to selected tenants.\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", + "description": "This function deploys SafeLinks policies and rules from templates to selected tenants.", "x-cipp-role": "Exchange.SafeLinks.ReadWrite", "requestBody": { "required": true, @@ -3596,13 +4732,15 @@ "type": "object", "properties": { "selectedTenants": { - "type": "string" + "type": "string", + "description": "selectedTenants parameter" }, "TemplateList": { "type": "array", "items": { "type": "string" - } + }, + "description": "TemplateList parameter" } } } @@ -3634,13 +4772,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -3702,13 +4840,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -3716,7 +4854,7 @@ }, "x-cipp-role": "CIPP.Scheduler.ReadWrite", "x-cipp-warnings": [ - "This endpoint does not use CippFormPage's customDataformatter — CippSchedulerForm calls ApiPostCall directly. The raw form values (with empty fields stripped) are posted as-is." + "This endpoint does not use CippFormPage's customDataformatter \u2014 CippSchedulerForm calls ApiPostCall directly. The raw form values (with empty fields stripped) are posted as-is." ], "parameters": [ { @@ -3724,7 +4862,8 @@ "in": "query", "required": false, "schema": { - "type": "string" + "type": "string", + "description": "Query parameter from PS1: hidden" } }, { @@ -3732,7 +4871,8 @@ "in": "query", "required": false, "schema": { - "type": "string" + "type": "string", + "description": "Query parameter from PS1: DisallowDuplicateName" } } ], @@ -3755,6 +4895,9 @@ "backup": { "$ref": "#/components/schemas/LabelValue" }, + "blockType": { + "$ref": "#/components/schemas/LabelValue" + }, "ca": { "type": "boolean" }, @@ -3767,12 +4910,21 @@ "CippWebhookAlerts": { "type": "boolean" }, + "dbCacheType": { + "$ref": "#/components/schemas/LabelValue" + }, + "dbFormat": { + "$ref": "#/components/schemas/LabelValue" + }, "email": { "type": "boolean" }, "groups": { "type": "boolean" }, + "includeRawAttachments": { + "type": "boolean" + }, "intunecompliance": { "type": "boolean" }, @@ -3788,8 +4940,27 @@ "psa": { "type": "boolean" }, + "removeRemediation": { + "type": "boolean" + }, + "scheduleName": { + "type": "string" + }, + "selectedTest": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LabelValue" + } + }, + "templateName": { + "type": "string" + }, + "testSuite": { + "$ref": "#/components/schemas/LabelValue" + }, "Trigger": { - "type": "object" + "type": "object", + "description": "Trigger configuration for triggered tasks. Contains Type, DeltaResource, EventType, ResourceFilter, WatchedAttributes, UseConditions, DeltaConditions, ExecutePerResource, ExecutionMode." }, "users": { "type": "boolean" @@ -3798,49 +4969,63 @@ "type": "boolean" }, "tenantFilter": { - "type": "object" + "type": "object", + "description": "Tenant selector object {value, label, type, addedFields}. 'AllTenants' is a valid value." }, "Name": { - "type": "string" + "type": "string", + "description": "Task name" }, "command": { - "type": "object" + "type": "object", + "description": "Command selector object {label, value, addedFields}" }, "taskType": { - "type": "object" + "type": "object", + "description": "{'scheduled'|'triggered'}. Determines which scheduling branch is active." }, "ScheduledTime": { - "type": "integer" + "type": "integer", + "description": "Unix timestamp (seconds) for scheduled start. Required when taskType=scheduled." }, "Recurrence": { - "type": "object" + "type": "object", + "description": "Recurrence option {label, value} e.g. {label:'Every 1 day', value:'1d'}" }, "parameters": { - "type": "object" + "type": "object", + "description": "Key-value map of command parameters. String[] types are sent as arrays." }, "RawJsonParameters": { - "type": "string" + "type": "string", + "description": "JSON string of parameters when advancedParameters mode is active. Used instead of 'parameters' field." }, "postExecution": { "type": "array", "items": { "type": "string" - } + }, + "description": "Array of post-execution action objects [{label, value}], e.g. Webhook, Email, PSA" }, "reference": { - "type": "string" + "type": "string", + "description": "Optional note/identifier for the task" }, "RowKey": { - "type": "string" + "type": "string", + "description": "Existing task RowKey when editing/re-running. PS1 uses this for RunNow scenario." }, "RunNow": { - "type": "boolean" + "type": "boolean", + "description": "If true, immediately re-runs the task identified by RowKey" }, "DesiredStartTime": { - "type": "string" + "type": "string", + "description": "Optional desired start time override passed to Add-CIPPScheduledTask" }, "DisallowDuplicateName": { - "type": "boolean" + "type": "boolean", + "description": "Prevent creating duplicate task names. Also readable from query string." } }, "required": [ @@ -3852,11 +5037,11 @@ } } }, - "/api/AddSharedMailbox": { + "/api/AddScriptedAlert": { "post": { - "summary": "AddSharedMailbox", + "summary": "AddScriptedAlert", "tags": [ - "Email-Exchange > Administration" + "Tenant > Administration > Alerts" ], "security": [ { @@ -3875,19 +5060,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "x-cipp-role": "CIPP.Alert.ReadWrite", "requestBody": { "required": true, "content": { @@ -3895,33 +5080,67 @@ "schema": { "type": "object", "properties": { - "tenantID": { + "Actions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LabelValue" + } + }, + "AlertComment": { "type": "string" }, - "displayName": { + "command": { + "$ref": "#/components/schemas/LabelValue" + }, + "CustomSubject": { "type": "string" }, - "username": { + "excludedTenants": { "type": "string" }, - "domain": { + "logbook": { + "$ref": "#/components/schemas/LabelValue" + }, + "postExecution": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LabelValue" + } + }, + "preset": { + "$ref": "#/components/schemas/LabelValue" + }, + "PSObject": { "type": "string" }, - "addedAliases": { + "recurrence": { + "$ref": "#/components/schemas/LabelValue" + }, + "RowKey": { + "type": "string" + }, + "startDateTime": { + "type": "string", + "format": "date-time" + }, + "tenantFilter": { "type": "string" } - } + }, + "required": [ + "tenantFilter" + ] } } } } } }, - "/api/AddSite": { + "/api/AddSensitiveInfoType": { "post": { - "summary": "AddSite", + "summary": "AddSensitiveInfoType", "tags": [ - "Teams-Sharepoint" + "Security > Compliance-SIT" ], "security": [ { @@ -3940,19 +5159,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Sharepoint.Site.ReadWrite", + "x-cipp-role": "Security.SensitiveInfoType.ReadWrite", "requestBody": { "required": true, "content": { @@ -3960,60 +5179,24 @@ "schema": { "type": "object", "properties": { - "sensitivityLabel": { - "type": "string" - }, - "siteDescription": { - "type": "string" - }, - "siteDesign": { - "$ref": "#/components/schemas/LabelValue", - "type": "object", - "properties": { - "value": { - "type": "string" - } - } - }, - "siteName": { + "PowerShellCommand": { "type": "string" }, - "siteOwner": { - "$ref": "#/components/schemas/LabelValue", - "type": "object", - "properties": { - "value": { - "type": "string" - } - } - }, - "templateName": { - "$ref": "#/components/schemas/LabelValue", - "type": "object", - "properties": { - "value": { - "type": "string" - } - } - }, - "tenantFilter": { + "selectedTenants": { "type": "string" } - }, - "required": [ - "tenantFilter" - ] + } } } } } } }, - "/api/AddSiteBulk": { + "/api/AddSensitiveInfoTypeTemplate": { "post": { - "summary": "AddSiteBulk", + "summary": "AddSensitiveInfoTypeTemplate", "tags": [ - "Teams-Sharepoint" + "Security > Compliance-SIT" ], "security": [ { @@ -4032,19 +5215,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Sharepoint.Site.ReadWrite", + "x-cipp-role": "Security.SensitiveInfoType.ReadWrite", "requestBody": { "required": true, "content": { @@ -4052,45 +5235,21 @@ "schema": { "type": "object", "properties": { - "bulkSites": { - "type": "string" - }, - "sensitivityLabel": { - "type": "string" - }, - "siteDescription": { - "type": "string" - }, - "siteDesign": { - "type": "string" - }, - "siteName": { - "type": "string" - }, - "siteOwner": { - "type": "string" - }, - "templateName": { - "type": "string" - }, - "tenantFilter": { + "PowerShellCommand": { "type": "string" } - }, - "required": [ - "tenantFilter" - ] + } } } } } } }, - "/api/AddSpamFilter": { + "/api/AddSensitivityLabel": { "post": { - "summary": "AddSpamFilter", + "summary": "AddSensitivityLabel", "tags": [ - "Email-Exchange > Spamfilter" + "Security > Compliance-SensitivityLabel" ], "security": [ { @@ -4109,19 +5268,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Exchange.SpamFilter.ReadWrite", + "x-cipp-role": "Security.SensitivityLabel.ReadWrite", "requestBody": { "required": true, "content": { @@ -4132,17 +5291,8 @@ "PowerShellCommand": { "type": "string" }, - "Priority": { - "type": "string" - }, "selectedTenants": { "type": "string" - }, - "TemplateList": { - "$ref": "#/components/schemas/LabelValue" - }, - "name": { - "type": "string" } } } @@ -4151,11 +5301,11 @@ } } }, - "/api/AddSpamFilterTemplate": { + "/api/AddSensitivityLabelTemplate": { "post": { - "summary": "AddSpamFilterTemplate", + "summary": "AddSensitivityLabelTemplate", "tags": [ - "Email-Exchange > Spamfilter" + "Security > Compliance-SensitivityLabel" ], "security": [ { @@ -4174,19 +5324,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Exchange.Spamfilter.ReadWrite", + "x-cipp-role": "Security.SensitivityLabel.ReadWrite", "requestBody": { "required": true, "content": { @@ -4194,9 +5344,6 @@ "schema": { "type": "object", "properties": { - "name": { - "type": "string" - }, "PowerShellCommand": { "type": "string" } @@ -4207,11 +5354,11 @@ } } }, - "/api/AddStandardsDeploy": { + "/api/AddSharedMailbox": { "post": { - "summary": "AddStandardsDeploy", + "summary": "AddSharedMailbox", "tags": [ - "Tenant > Standards" + "Email-Exchange > Administration" ], "security": [ { @@ -4230,19 +5377,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Tenant.Standards.ReadWrite", + "x-cipp-role": "Exchange.Mailbox.ReadWrite", "requestBody": { "required": true, "content": { @@ -4250,8 +5397,24 @@ "schema": { "type": "object", "properties": { - "tenant": { + "tenantID": { + "type": "string", + "description": "Tenant domain/ID. Formatter reads from useSettings().currentTenant and sends as 'tenantID' (not tenantFilter)." + }, + "displayName": { "type": "string" + }, + "username": { + "type": "string", + "description": "Mailbox alias (local part of email; combined with domain server-side as username@domain)" + }, + "domain": { + "type": "string", + "description": "Domain extracted from autocomplete object .value by formatter" + }, + "addedAliases": { + "type": "string", + "description": "Newline-separated aliases; PS1 splits on \\n. Not sent by default form but accepted." } } } @@ -4260,11 +5423,11 @@ } } }, - "/api/AddStandardsTemplate": { + "/api/AddSite": { "post": { - "summary": "AddStandardsTemplate", + "summary": "AddSite", "tags": [ - "Tenant > Standards" + "Teams-Sharepoint" ], "security": [ { @@ -4283,19 +5446,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Tenant.Standards.ReadWrite", + "x-cipp-role": "Sharepoint.Site.ReadWrite", "requestBody": { "required": true, "content": { @@ -4303,17 +5466,66 @@ "schema": { "type": "object", "properties": { - "createdAt": { - "type": "string" + "sensitivityLabel": { + "type": "string", + "description": "sensitivityLabel parameter" }, - "GUID": { - "type": "string" + "siteDescription": { + "type": "string", + "description": "siteDescription parameter" + }, + "siteDesign": { + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "siteDesign parameter", + "type": "object", + "properties": { + "value": { + "type": "string", + "description": "Body parameter from PS1: siteDesign.value" + } + } + }, + "siteName": { + "type": "string", + "description": "siteName parameter" + }, + "siteOwner": { + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "siteOwner parameter", + "type": "object", + "properties": { + "value": { + "type": "string", + "description": "Body parameter from PS1: siteOwner.value" + } + } }, "templateName": { - "type": "string" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "templateName parameter", + "type": "object", + "properties": { + "value": { + "type": "string", + "description": "Body parameter from PS1: templateName.value" + } + } }, "tenantFilter": { - "type": "string" + "type": "string", + "description": "tenantFilter parameter" } }, "required": [ @@ -4325,11 +5537,11 @@ } } }, - "/api/AddStoreApp": { + "/api/AddSiteBulk": { "post": { - "summary": "AddStoreApp", + "summary": "AddSiteBulk", "tags": [ - "Endpoint > Applications" + "Teams-Sharepoint" ], "security": [ { @@ -4348,19 +5560,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Endpoint.Application.ReadWrite", + "x-cipp-role": "Sharepoint.Site.ReadWrite", "requestBody": { "required": true, "content": { @@ -4368,39 +5580,53 @@ "schema": { "type": "object", "properties": { - "ApplicationName": { - "type": "string" + "bulkSites": { + "type": "string", + "description": "bulkSites parameter" }, - "AssignTo": { - "type": "string" + "sensitivityLabel": { + "type": "string", + "description": "sensitivityLabel parameter" }, - "CustomGroup": { - "type": "string" + "siteDescription": { + "type": "string", + "description": "siteDescription parameter" }, - "description": { - "type": "string" + "siteDesign": { + "type": "string", + "description": "siteDesign parameter" }, - "InstallationIntent": { - "type": "string" + "siteName": { + "type": "string", + "description": "siteName parameter" }, - "PackageName": { - "type": "string" + "siteOwner": { + "type": "string", + "description": "siteOwner parameter" }, - "selectedTenants": { - "type": "string" + "templateName": { + "type": "string", + "description": "templateName parameter" + }, + "tenantFilter": { + "type": "string", + "description": "tenantFilter parameter" } - } + }, + "required": [ + "tenantFilter" + ] } } } } } }, - "/api/AddTeam": { + "/api/AddSpamFilter": { "post": { - "summary": "AddTeam", + "summary": "AddSpamFilter", "tags": [ - "Teams-Sharepoint" + "Email-Exchange > Spamfilter" ], "security": [ { @@ -4419,19 +5645,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Teams.Group.ReadWrite", + "x-cipp-role": "Exchange.SpamFilter.ReadWrite", "requestBody": { "required": true, "content": { @@ -4439,20 +5665,29 @@ "schema": { "type": "object", "properties": { - "displayName": { - "type": "string" + "PowerShellCommand": { + "type": "string", + "description": "PowerShellCommand parameter" }, - "description": { - "type": "string" + "Priority": { + "type": "string", + "description": "Priority parameter" }, - "owner": { - "type": "string" + "selectedTenants": { + "type": "string", + "description": "selectedTenants parameter" }, - "visibility": { - "type": "string" + "TemplateList": { + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "TemplateList parameter" }, - "tenantid": { - "type": "string" + "name": { + "type": "string", + "description": "Body parameter from PS1: name" } } } @@ -4461,11 +5696,11 @@ } } }, - "/api/AddTenant": { + "/api/AddSpamFilterTemplate": { "post": { - "summary": "AddTenant", + "summary": "AddSpamFilterTemplate", "tags": [ - "Tenant > Administration > Tenant" + "Email-Exchange > Spamfilter" ], "security": [ { @@ -4484,37 +5719,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Tenant.Config.ReadWrite", - "parameters": [ - { - "name": "Action", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "TenantName", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - } - ], + "x-cipp-role": "Exchange.Spamfilter.ReadWrite", "requestBody": { "required": true, "content": { @@ -4522,43 +5739,10 @@ "schema": { "type": "object", "properties": { - "Action": { - "type": "string" - }, - "AddressLine1": { - "type": "string" - }, - "AddressLine2": { - "type": "string" - }, - "City": { - "type": "string" - }, - "CompanyName": { - "type": "string" - }, - "Country": { - "type": "string" - }, - "Email": { - "type": "string" - }, - "FirstName": { - "type": "string" - }, - "LastName": { - "type": "string" - }, - "PhoneNumber": { - "type": "string" - }, - "PostalCode": { - "type": "string" - }, - "State": { + "name": { "type": "string" }, - "TenantName": { + "PowerShellCommand": { "type": "string" } } @@ -4568,11 +5752,11 @@ } } }, - "/api/AddTenantAllowBlockList": { + "/api/AddStandardsDeploy": { "post": { - "summary": "AddTenantAllowBlockList", + "summary": "AddStandardsDeploy", "tags": [ - "Email-Exchange > Spamfilter" + "Tenant > Standards" ], "security": [ { @@ -4591,19 +5775,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Exchange.SpamFilter.ReadWrite", + "x-cipp-role": "Tenant.Standards.ReadWrite", "requestBody": { "required": true, "content": { @@ -4611,25 +5795,7 @@ "schema": { "type": "object", "properties": { - "entries": { - "type": "string" - }, - "listMethod": { - "$ref": "#/components/schemas/LabelValue" - }, - "listType": { - "$ref": "#/components/schemas/LabelValue" - }, - "NoExpiration": { - "type": "boolean" - }, - "notes": { - "type": "string" - }, - "RemoveAfter": { - "type": "boolean" - }, - "tenantID": { + "tenant": { "type": "string" } } @@ -4639,11 +5805,11 @@ } } }, - "/api/AddTestReport": { + "/api/AddStandardsTemplate": { "post": { - "summary": "AddTestReport", + "summary": "AddStandardsTemplate", "tags": [ - "Uncategorized" + "Tenant > Standards" ], "security": [ { @@ -4662,19 +5828,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "CIPP.Dashboard.Read", + "x-cipp-role": "Tenant.Standards.ReadWrite", "requestBody": { "required": true, "content": { @@ -4682,30 +5848,36 @@ "schema": { "type": "object", "properties": { - "description": { + "createdAt": { "type": "string" }, - "DevicesTests": { + "GUID": { "type": "string" }, - "IdentityTests": { + "policySource": { + "$ref": "#/components/schemas/LabelValue" + }, + "templateName": { "type": "string" }, - "name": { + "tenantFilter": { "type": "string" } - } + }, + "required": [ + "tenantFilter" + ] } } } } } }, - "/api/AddTransportRule": { + "/api/AddStoreApp": { "post": { - "summary": "AddTransportRule", + "summary": "AddStoreApp", "tags": [ - "Email-Exchange > Transport" + "Endpoint > Applications" ], "security": [ { @@ -4724,19 +5896,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Exchange.TransportRule.ReadWrite", + "x-cipp-role": "Endpoint.Application.ReadWrite", "requestBody": { "required": true, "content": { @@ -4744,20 +5916,33 @@ "schema": { "type": "object", "properties": { - "PowerShellCommand": { - "type": "string" + "ApplicationName": { + "type": "string", + "description": "ApplicationName parameter" }, - "selectedTenants": { - "type": "string" + "AssignTo": { + "type": "string", + "description": "AssignTo parameter" }, - "TemplateList": { - "$ref": "#/components/schemas/LabelValue" + "CustomGroup": { + "type": "string", + "description": "CustomGroup parameter" }, - "PSObject": { - "type": "string" + "description": { + "type": "string", + "description": "description parameter" }, - "name": { - "type": "string" + "InstallationIntent": { + "type": "string", + "description": "InstallationIntent parameter" + }, + "PackageName": { + "type": "string", + "description": "PackageName parameter" + }, + "selectedTenants": { + "type": "string", + "description": "selectedTenants parameter" } } } @@ -4766,11 +5951,11 @@ } } }, - "/api/AddTransportTemplate": { + "/api/AddTeam": { "post": { - "summary": "AddTransportTemplate", + "summary": "AddTeam", "tags": [ - "Email-Exchange > Transport" + "Teams-Sharepoint" ], "security": [ { @@ -4789,19 +5974,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Exchange.TransportRule.ReadWrite", + "x-cipp-role": "Teams.Group.ReadWrite", "requestBody": { "required": true, "content": { @@ -4809,11 +5994,23 @@ "schema": { "type": "object", "properties": { - "name": { + "displayName": { "type": "string" }, - "PowerShellCommand": { + "description": { "type": "string" + }, + "owner": { + "type": "string", + "description": "Owner UPN extracted from user selector .value by formatter. PS1 passes as member@odata.bind with role 'owner'." + }, + "visibility": { + "type": "string", + "description": "Enum: 'public' | 'private'. Default 'public'." + }, + "tenantid": { + "type": "string", + "description": "Body parameter from PS1: tenantid" } } } @@ -4822,11 +6019,11 @@ } } }, - "/api/AddUser": { + "/api/AddTenant": { "post": { - "summary": "AddUser", + "summary": "AddTenant", "tags": [ - "Identity > Administration > Users" + "Tenant > Administration > Tenant" ], "security": [ { @@ -4845,13 +6042,464 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Config.ReadWrite", + "parameters": [ + { + "name": "Action", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "TenantName", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Action": { + "type": "string" + }, + "AddressLine1": { + "type": "string" + }, + "AddressLine2": { + "type": "string" + }, + "City": { + "type": "string" + }, + "CompanyName": { + "type": "string" + }, + "Country": { + "type": "string" + }, + "Email": { + "type": "string" + }, + "FirstName": { + "type": "string" + }, + "LastName": { + "type": "string" + }, + "PhoneNumber": { + "type": "string" + }, + "PostalCode": { + "type": "string" + }, + "State": { + "type": "string" + }, + "TenantName": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/AddTenantAllowBlockList": { + "post": { + "summary": "AddTenantAllowBlockList", + "tags": [ + "Email-Exchange > Spamfilter" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.SpamFilter.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "entries": { + "type": "string" + }, + "listMethod": { + "$ref": "#/components/schemas/LabelValue" + }, + "listType": { + "$ref": "#/components/schemas/LabelValue" + }, + "NoExpiration": { + "type": "boolean" + }, + "notes": { + "type": "string" + }, + "RemoveAfter": { + "type": "boolean" + }, + "tenantID": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/AddTenantAllowBlockListTemplate": { + "post": { + "summary": "AddTenantAllowBlockListTemplate", + "tags": [ + "Email-Exchange > Spamfilter" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.SpamFilter.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "entries": { + "type": "string" + }, + "listMethod": { + "$ref": "#/components/schemas/LabelValue" + }, + "listType": { + "$ref": "#/components/schemas/LabelValue" + }, + "NoExpiration": { + "type": "boolean" + }, + "notes": { + "type": "string" + }, + "RemoveAfter": { + "type": "boolean" + }, + "templateName": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/AddTestReport": { + "post": { + "summary": "AddTestReport", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Dashboard.Read", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "CustomTests": { + "type": "string" + }, + "name": { + "type": "string", + "description": "name parameter" + }, + "ReportId": { + "type": "string" + }, + "description": { + "type": "string", + "description": "description parameter" + }, + "DevicesTests": { + "type": "string", + "description": "DevicesTests parameter" + }, + "IdentityTests": { + "type": "string", + "description": "IdentityTests parameter" + } + } + } + } + } + } + } + }, + "/api/AddTransportRule": { + "post": { + "summary": "AddTransportRule", + "tags": [ + "Email-Exchange > Transport" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.TransportRule.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "PowerShellCommand": { + "type": "string", + "description": "PowerShellCommand parameter" + }, + "selectedTenants": { + "type": "string", + "description": "selectedTenants parameter" + }, + "TemplateList": { + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "TemplateList parameter" + }, + "PSObject": { + "type": "string", + "description": "Body parameter from PS1: PSObject" + }, + "name": { + "type": "string", + "description": "Body parameter from PS1: name" + } + } + } + } + } + } + } + }, + "/api/AddTransportTemplate": { + "post": { + "summary": "AddTransportTemplate", + "tags": [ + "Email-Exchange > Transport" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.TransportRule.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "PowerShellCommand": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/AddUser": { + "post": { + "summary": "AddUser", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -4868,7 +6516,8 @@ "type": "object", "properties": { "tenantFilter": { - "type": "string" + "type": "string", + "description": "Target tenant domain" }, "givenName": { "type": "string" @@ -4877,10 +6526,12 @@ "type": "string" }, "username": { - "type": "string" + "type": "string", + "description": "UPN prefix (before the @)" }, "password": { - "type": "string" + "type": "string", + "description": "Initial password. Omit to auto-generate." }, "MustChangePass": { "type": "boolean" @@ -4892,7 +6543,8 @@ "type": "array", "items": { "type": "string" - } + }, + "description": "SKU IDs to assign" }, "jobTitle": { "type": "string" @@ -4928,42 +6580,60 @@ } }, "copyFrom": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "Copy group memberships from this user" }, "userTemplate": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "Apply a saved user template" }, "Scheduled": { "$ref": "#/components/schemas/ScheduledTask", "type": "object", "properties": { "date": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: Scheduled.date" }, "Enabled": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: Scheduled.Enabled" } } }, "reference": { - "type": "string" + "type": "string", + "description": "External reference ID (PSA ticket, etc.)" }, "PostExecution": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: PostExecution" }, "mailNickname": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: mailNickname" }, "PrimDomain": { "type": "object", + "description": "Body parameter from PS1: PrimDomain", "properties": { "value": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: PrimDomain.value" } } }, "DisplayName": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: DisplayName" } }, "required": [ @@ -4998,13 +6668,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -5019,28 +6689,38 @@ "type": "object", "properties": { "tenantFilter": { - "type": "string" + "type": "string", + "description": "Target tenant domain" }, "BulkUser": { "type": "array", "items": { "type": "string" - } + }, + "description": "Array of user objects to create. Each element is a user row (typically from CSV import)." }, "licenses": { "type": "array", "items": { "type": "string" - } + }, + "description": "License SKU IDs to assign to all created users. Accepts array of GUID strings or LabelValue objects." }, "usageLocation": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "Usage location applied to all created users. Accepts LabelValue or raw ISO country code string." }, "value": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: value" }, "label": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: label" } }, "required": [ @@ -5075,13 +6755,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -5099,6 +6779,18 @@ "type": "string", "description": "Additional SMTP aliases, newline-delimited (server splits on \\n)" }, + "addToGroups": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GroupRef" + } + }, + "businessPhones": { + "type": "array", + "items": { + "type": "string" + } + }, "city": { "type": "string" }, @@ -5123,6 +6815,9 @@ "givenName": { "type": "string" }, + "groupMemberships": { + "type": "string" + }, "GUID": { "type": "string" }, @@ -5186,6 +6881,12 @@ }, "usernameFormat": { "type": "string" + }, + "usernameSpaceHandling": { + "type": "string" + }, + "usernameSpaceReplacement": { + "type": "string" } }, "required": [ @@ -5220,13 +6921,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -5240,50 +6941,68 @@ "schema": { "type": "object", "properties": { - "AssignTo": { + "detectionScript": { "type": "string" }, + "AssignTo": { + "type": "string", + "description": "AssignTo parameter" + }, "CustomGroup": { - "type": "string" + "type": "string", + "description": "CustomGroup parameter" }, "description": { - "type": "string" + "type": "string", + "description": "description parameter" }, "detectionFile": { - "type": "string" + "type": "string", + "description": "detectionFile parameter" }, "detectionPath": { - "type": "string" + "type": "string", + "description": "detectionPath parameter" }, "DisableRestart": { - "type": "string" + "type": "string", + "description": "DisableRestart parameter" }, "enforceSignatureCheck": { - "type": "string" + "type": "string", + "description": "enforceSignatureCheck parameter" }, "InstallAsSystem": { - "type": "string" + "type": "string", + "description": "InstallAsSystem parameter" }, "InstallationIntent": { - "type": "string" + "type": "string", + "description": "InstallationIntent parameter" }, "installScript": { - "type": "string" + "type": "string", + "description": "installScript parameter" }, "publisher": { - "type": "string" + "type": "string", + "description": "publisher parameter" }, "runAs32Bit": { - "type": "string" + "type": "string", + "description": "runAs32Bit parameter" }, "selectedTenants": { - "type": "string" + "type": "string", + "description": "selectedTenants parameter" }, "uninstallScript": { - "type": "string" + "type": "string", + "description": "uninstallScript parameter" }, "applicationName": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: applicationName" } } } @@ -5315,13 +7034,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -5330,11 +7049,11 @@ "x-cipp-role": "Tenant.BestPracticeAnalyser.Read" } }, - "/api/CIPPDBTestsRun": { - "get": { - "summary": "CIPPDBTestsRun", + "/api/CreateSafeLinksPolicyTemplate": { + "post": { + "summary": "This function creates a new Safe Links policy template from scratch.", "tags": [ - "Activity Triggers > Tests" + "Security > Safe-Links-Policy" ], "security": [ { @@ -5353,132 +7072,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Tenant.Tests.Read" - } - }, - "/api/CIPPOffboardingJob": { - "get": { - "summary": "CIPPOffboardingJob", - "tags": [ - "Identity > Administration > Users" - ], - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StandardResults" - } - } - } - }, - "400": { - "description": "Bad request — missing required field or invalid input" - }, - "401": { - "description": "Unauthorized — invalid or missing bearer token" - }, - "403": { - "description": "Forbidden — caller lacks the required RBAC role" - }, - "500": { - "description": "Internal server error" - } - } - } - }, - "/api/CIPPStandardsRun": { - "get": { - "summary": "CIPPStandardsRun", - "tags": [ - "Tenant > Standards" - ], - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StandardResults" - } - } - } - }, - "400": { - "description": "Bad request — missing required field or invalid input" - }, - "401": { - "description": "Unauthorized — invalid or missing bearer token" - }, - "403": { - "description": "Forbidden — caller lacks the required RBAC role" - }, - "500": { - "description": "Internal server error" - } - }, - "x-cipp-role": "Tenant.Standards.ReadWrite" - } - }, - "/api/CreateSafeLinksPolicyTemplate": { - "post": { - "summary": "CreateSafeLinksPolicyTemplate", - "tags": [ - "Security > Safe-Links-Policy" - ], - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StandardResults" - } - } - } - }, - "400": { - "description": "Bad request — missing required field or invalid input" - }, - "401": { - "description": "Unauthorized — invalid or missing bearer token" - }, - "403": { - "description": "Forbidden — caller lacks the required RBAC role" - }, - "500": { - "description": "Internal server error" - } - }, - "description": "This function creates a new Safe Links policy template from scratch.\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", + "description": "This function creates a new Safe Links policy template from scratch.", "x-cipp-role": "Exchange.SafeLinks.ReadWrite", "requestBody": { "required": true, @@ -5494,10 +7100,12 @@ "type": "string" }, "PolicyName": { - "type": "string" + "type": "string", + "description": "Policy name to store in template" }, "AdminDisplayName": { - "type": "string" + "type": "string", + "description": "Policy description. Formatter also sets this to Description if AdminDisplayName is empty." }, "EnableSafeLinksForEmail": { "type": "boolean" @@ -5533,19 +7141,22 @@ "type": "array", "items": { "type": "string" - } + }, + "description": "Array of URL/domain/wildcard strings" }, "CustomNotificationText": { "type": "string" }, "RuleName": { - "type": "string" + "type": "string", + "description": "Auto-generated as '{PolicyName}_Rule' by the frontend" }, "Priority": { "type": "integer" }, "State": { - "type": "boolean" + "type": "boolean", + "description": "Rule enabled state" }, "Comments": { "type": "string" @@ -5554,19 +7165,22 @@ "type": "array", "items": { "type": "string" - } + }, + "description": "Array of recipient UPNs" }, "SentToMemberOf": { "type": "array", "items": { "type": "string" - } + }, + "description": "Array of group IDs" }, "RecipientDomainIs": { "type": "array", "items": { "type": "string" - } + }, + "description": "Array of domain strings" }, "ExceptIfSentTo": { "type": "array", @@ -5587,7 +7201,8 @@ } }, "tenantFilter": { - "type": "string" + "type": "string", + "description": "Not consumed by PS1 (AnyTenant endpoint) but included by formatter" } }, "required": [ @@ -5622,13 +7237,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -5681,13 +7296,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -5713,7 +7328,7 @@ }, "/api/DeployContactTemplates": { "post": { - "summary": "DeployContactTemplates", + "summary": "This function deploys contact(s) from template(s) to selected tenants.", "tags": [ "Email-Exchange > Administration > Contacts" ], @@ -5734,19 +7349,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "description": "This function deploys contact(s) from template(s) to selected tenants.\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", + "description": "This function deploys contact(s) from template(s) to selected tenants.", "x-cipp-role": "Exchange.Contact.ReadWrite", "requestBody": { "required": true, @@ -5756,16 +7371,19 @@ "type": "object", "properties": { "selectedTenants": { - "type": "string" + "type": "string", + "description": "selectedTenants parameter" }, "TemplateList": { "type": "object", "items": { "type": "string" }, + "description": "TemplateList parameter", "properties": { "Count": { - "type": "string" + "type": "string", + "description": "TemplateList.Count parameter" } } } @@ -5799,13 +7417,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -5882,13 +7500,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -5903,29 +7521,36 @@ "type": "object", "properties": { "displayName": { - "type": "string" + "type": "string", + "description": "(auto) from CippAddAssignmentFilterForm.jsx" }, "description": { - "type": "string" + "type": "string", + "description": "(auto) from CippAddAssignmentFilterForm.jsx" }, "assignmentFilterManagementType": { "type": "string", "enum": [ "devices", "apps" - ] + ], + "description": "(auto) from CippAddAssignmentFilterForm.jsx" }, "platform": { - "type": "string" + "type": "string", + "description": "(auto) from CippAddAssignmentFilterForm.jsx" }, "rule": { - "type": "string" + "type": "string", + "description": "(auto) from CippAddAssignmentFilterForm.jsx" }, "filterId": { - "type": "string" + "type": "string", + "description": "(auto) from API scan" }, "tenantFilter": { - "type": "string" + "type": "string", + "description": "(auto) from API scan" } }, "required": [ @@ -5960,13 +7585,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -6054,13 +7679,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -6075,51 +7700,173 @@ "type": "object", "properties": { "tenantID": { - "type": "string" + "type": "string", + "description": "Tenant domain/ID. Formatter reads from useSettings().currentTenant and sends as 'tenantID'." }, "ContactID": { - "type": "string" + "type": "string", + "description": "Graph contact id; read from API response. PS1 uses as Identity for Set-Contact/Set-MailContact." }, "displayName": { - "type": "string" + "type": "string", + "description": "Sent as lowercase 'displayName'; PS1 maps to 'DisplayName' parameter of Set-Contact." }, "hidefromGAL": { - "type": "boolean" + "type": "boolean", + "description": "PS1 uses as HiddenFromAddressListsEnabled on Set-MailContact." }, "email": { - "type": "string" + "type": "string", + "description": "PS1 maps to WindowsEmailAddress on Set-Contact." }, "firstName": { - "type": "string" + "type": "string", + "description": "PS1 maps to FirstName." }, "LastName": { - "type": "string" + "type": "string", + "description": "NOTE: formatter sends form field 'lastName' as-is but PS1 reads $contactInfo.LastName (capital L). Matches because object property access is case-insensitive in PS." }, "Title": { - "type": "string" + "type": "string", + "description": "Formatter maps form 'jobTitle' \u2192 body field 'Title'. PS1 reads $contactInfo.Title." }, "StreetAddress": { - "type": "string" + "type": "string", + "description": "Formatter maps 'streetAddress' \u2192 'StreetAddress'. PS1 reads $contactInfo.StreetAddress." }, "PostalCode": { - "type": "string" + "type": "string", + "description": "Formatter maps 'postalCode' \u2192 'PostalCode'." }, "City": { - "type": "string" + "type": "string", + "description": "Formatter maps 'city' \u2192 'City'." }, "State": { - "type": "string" + "type": "string", + "description": "Formatter maps 'state' \u2192 'State'. PS1 maps to StateOrProvince." }, "CountryOrRegion": { - "type": "string" + "type": "string", + "description": "Formatter maps 'country' autocomplete \u2192 'CountryOrRegion' (unwraps .value)." }, "Company": { + "type": "string", + "description": "Formatter maps 'companyName' \u2192 'Company'." + }, + "mobilePhone": { + "type": "string", + "description": "PS1 maps to MobilePhone." + }, + "phone": { + "type": "string", + "description": "Formatter maps 'businessPhone' \u2192 'phone'. PS1 maps to Phone." + }, + "website": { + "type": "string", + "description": "PS1 maps to WebPage." + }, + "mailTip": { + "type": "string", + "description": "PS1 maps to MailTip on Set-MailContact." + } + } + } + } + } + } + } + }, + "/api/EditContactTemplates": { + "post": { + "summary": "EditContactTemplates", + "tags": [ + "Email-Exchange > Administration > Contacts" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Contact.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ContactTemplateID": { + "type": "string", + "description": "GUID of the template to update. PS1 uses this as the RowKey filter." + }, + "displayName": { + "type": "string", + "description": "Template display name; also used as 'name' key internally." + }, + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "email": { + "type": "string" + }, + "hidefromGAL": { + "type": "boolean" + }, + "streetAddress": { + "type": "string" + }, + "postalCode": { + "type": "string" + }, + "city": { + "type": "string" + }, + "state": { + "type": "string" + }, + "country": { + "type": "string", + "description": "2-letter ISO code (unwrapped from autocomplete object by formatter)" + }, + "companyName": { "type": "string" }, "mobilePhone": { "type": "string" }, - "phone": { + "businessPhone": { + "type": "string" + }, + "jobTitle": { "type": "string" }, "website": { @@ -6135,11 +7882,11 @@ } } }, - "/api/EditContactTemplates": { + "/api/EditDlpCompliancePolicy": { "post": { - "summary": "EditContactTemplates", + "summary": "EditDlpCompliancePolicy", "tags": [ - "Email-Exchange > Administration > Contacts" + "Security > Compliance-DLP" ], "security": [ { @@ -6158,19 +7905,40 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Exchange.Contact.ReadWrite", + "x-cipp-role": "Security.DlpCompliancePolicy.ReadWrite", + "parameters": [ + { + "name": "Identity", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "State", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ], "requestBody": { "required": true, "content": { @@ -6178,58 +7946,25 @@ "schema": { "type": "object", "properties": { - "ContactTemplateID": { - "type": "string" - }, - "displayName": { - "type": "string" - }, - "firstName": { - "type": "string" - }, - "lastName": { - "type": "string" - }, - "email": { - "type": "string" - }, - "hidefromGAL": { - "type": "boolean" - }, - "streetAddress": { - "type": "string" - }, - "postalCode": { - "type": "string" - }, - "city": { - "type": "string" - }, - "state": { - "type": "string" - }, - "country": { - "type": "string" - }, - "companyName": { - "type": "string" - }, - "mobilePhone": { + "Identity": { "type": "string" }, - "businessPhone": { + "Name": { "type": "string" }, - "jobTitle": { + "parameters": { "type": "string" }, - "website": { + "State": { "type": "string" }, - "mailTip": { + "tenantFilter": { "type": "string" } - } + }, + "required": [ + "tenantFilter" + ] } } } @@ -6259,13 +7994,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -6283,10 +8018,12 @@ "type": "object", "properties": { "tenantID": { - "type": "string" + "type": "string", + "description": "Tenant domain. Formatter reads from useSettings().currentTenant." }, "equipmentId": { - "type": "string" + "type": "string", + "description": "Equipment mailbox identity (from router query). PS1 uses for Set-Mailbox, Set-User, Set-CalendarProcessing, Set-MailboxCalendarConfiguration Identity." }, "hiddenFromAddressListsEnabled": { "type": "boolean" @@ -6298,7 +8035,8 @@ "type": "string" }, "streetAddress": { - "type": "string" + "type": "string", + "description": "PS1 Set-User param name is 'StreetAddress' but reads $MailboxObject.streetAddress (case-insensitive)" }, "city": { "type": "string" @@ -6310,7 +8048,8 @@ "type": "string" }, "countryOrRegion": { - "type": "string" + "type": "string", + "description": "2-letter country code; formatter unwraps autocomplete .value" }, "phone": { "type": "string" @@ -6319,7 +8058,8 @@ "type": "array", "items": { "type": "string" - } + }, + "description": "Array of strings; formatter maps tag objects to .value strings" }, "allowConflicts": { "type": "boolean" @@ -6343,10 +8083,12 @@ "type": "boolean" }, "automateProcessing": { - "type": "string" + "type": "string", + "description": "Enum: None|AutoUpdate|AutoAccept. Formatter unwraps autocomplete .value." }, "workDays": { - "type": "string" + "type": "string", + "description": "Comma-separated string of work days. Formatter joins array of {value} objects." }, "workHoursStartTime": { "type": "string" @@ -6355,13 +8097,16 @@ "type": "string" }, "workingHoursTimeZone": { - "type": "string" + "type": "string", + "description": "Timezone standard name. Formatter unwraps autocomplete .value." }, "userPrincipalName": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: userPrincipalName" }, "DisplayName": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: DisplayName" } } } @@ -6393,13 +8138,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -6487,13 +8232,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -6510,28 +8255,47 @@ "schema": { "type": "object", "properties": { + "AddLicenses": { + "type": "array", + "items": { + "type": "string" + } + }, + "RemoveLicenses": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LabelValue" + } + }, "tenantFilter": { - "type": "string" + "type": "string", + "description": "Tenant domain. PS1 reads $UserObj.tenantFilter and falls back to $UserObj.tenantId." }, "groupId": { "type": "object", + "description": "Group ID string. PS1 reads $UserObj.groupId.value ?? $UserObj.groupId.", "properties": { "addedFields.groupName": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: groupId.addedFields.groupName" }, "value": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: groupId.value" }, "addedFields.groupType": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: groupId.addedFields.groupType" } } }, "groupType": { - "type": "string" + "type": "string", + "description": "Group type string e.g. 'Microsoft 365', 'Security', 'Distribution List', 'Mail-Enabled Security'. PS1 reads $UserObj.groupId.addedFields.groupType ?? $UserObj.groupType." }, "displayName": { - "type": "string" + "type": "string", + "description": "New display name. PS1 reads as groupName ?? displayName ?? groupId.addedFields.groupName." }, "description": { "type": "string" @@ -6540,67 +8304,82 @@ "type": "string" }, "membershipRules": { - "type": "string" + "type": "string", + "description": "Dynamic membership rule expression. Only used for DynamicMembership groups." }, "securityEnabled": { - "type": "boolean" + "type": "boolean", + "description": "Only sent if changed from initial value (customDataformatter deletes unchanged properties)." }, "visibility": { - "type": "string" + "type": "string", + "description": "'Public'|'Private'. Only sent if changed from initial value." }, "allowExternal": { - "type": "boolean" + "type": "boolean", + "description": "Allow external senders. Only sent if changed." }, "sendCopies": { - "type": "boolean" + "type": "boolean", + "description": "Send copies to member inboxes (M365 groups). Only sent if changed." }, "hideFromOutlookClients": { - "type": "boolean" + "type": "boolean", + "description": "Hide group from Outlook (M365 groups). Only sent if changed." }, "mail": { - "type": "string" + "type": "string", + "description": "Group email address; used for ExO DL operations." }, "AddMember": { "type": "array", "items": { "type": "string" - } + }, + "description": "Array of user objects to add as members [{value: id, addedFields: {userPrincipalName, displayName, id}}]." }, "RemoveMember": { "type": "array", "items": { "type": "string" - } + }, + "description": "Array of user objects to remove [{value: id, addedFields: {userPrincipalName}}]." }, "AddOwner": { "type": "array", "items": { "type": "string" - } + }, + "description": "Array of user objects to add as owners." }, "RemoveOwner": { "type": "array", "items": { "type": "string" - } + }, + "description": "Array of user objects to remove from owners." }, "AddContact": { "type": "array", "items": { "type": "string" - } + }, + "description": "Array of contact objects to add [{value: email/guid, addedFields: {id, displayName, WindowsEmailAddress}}]." }, "RemoveContact": { "type": "array", "items": { "type": "string" - } + }, + "description": "Array of contact objects to remove [{value: mail/guid, addedFields: {id}}]." }, "groupName": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: groupName" }, "tenantId": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: tenantId" } }, "required": [ @@ -6635,19 +8414,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Endpoint.MEM.Read", + "x-cipp-role": "Endpoint.MEM.ReadWrite", "parameters": [ { "name": "ID", @@ -6729,13 +8508,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -6807,13 +8586,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -6827,56 +8606,118 @@ "schema": { "type": "object", "properties": { + "defaultUseGroups": { + "type": "boolean" + }, + "defaultUseRoles": { + "type": "boolean" + }, + "GUID": { + "type": "string", + "description": "GUID of the template to edit. Retrieve from GET /api/ListJITAdminTemplates." + }, "defaultDomain": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "Pre-selects a domain when userAction is \"create\". LabelValue shape: { \"label\": \"contoso.com\", \"value\": \"contoso.com\" }. The value is the domain string. Accepts a plain string." }, "defaultDuration": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "Default JIT access duration. LabelValue shape: { \"label\": \"4 Hours\", \"value\": \"PT4H\" }. The value must be an ISO 8601 duration string. Common presets: \"PT1H\" (1 hr), \"PT4H\" (4 hrs), \"PT8H\" (8 hrs), \"P1D\" (1 day), \"P3D\", \"P7D\", \"P14D\", \"P30D\". Custom durations like \"PT2H30M\" are also valid. Cannot exceed the global MaxDuration set in JIT Admin Settings." }, "defaultExistingUser": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "Pre-selects a user when defaultUserAction is \"select\". LabelValue shape: { \"label\": \"User Display Name\", \"value\": \"\" }. The PS1 accepts either a UPN or an ObjectId GUID as the value." }, "defaultExpireAction": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "Default expiration action for JIT sessions using this template. LabelValue shape: { \"label\": \"...\", \"value\": \"\" }. Valid values: \"DisableUser\", \"DeleteUser\", \"RemoveRoles\" (select only), \"RemoveGroups\" (select only), \"RemoveRolesAndGroups\" (select only). The \"Remove*\" variants require defaultUserAction=\"select\"." }, "defaultFirstName": { - "type": "string" + "type": "string", + "description": "Pre-fills the first name field when defaultUserAction is \"create\"." }, "defaultForTenant": { - "type": "boolean" + "type": "boolean", + "description": "If true, marks this template as the default for the target tenant. Only one template can be the tenant default." + }, + "defaultGroups": { + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "Array of groups pre-selected in the template. LabelValue shape: { \"label\": \"Group Display Name\", \"value\": \"\" }. The value must be the group's ObjectId (GUID). Stored as-is and applied when a JIT session is created from this template." }, "defaultLastName": { - "type": "string" + "type": "string", + "description": "Pre-fills the last name field when defaultUserAction is \"create\"." }, "defaultNotificationActions": { "type": "array", "items": { "type": "string" - } + }, + "description": "Default notification actions for JIT sessions created from this template. Array of strings. Valid values: \"Webhook\", \"email\", \"PSA\"." }, "defaultRoles": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "Array of admin roles pre-selected in the template. LabelValue shape: { \"label\": \"Role Display Name\", \"value\": \"\" }. The value must be the role's ObjectId (GUID). Retrieve available roles from GET /api/ListRoles?TenantFilter=. Stored as-is and applied when a JIT session is created from this template." }, "defaultUserAction": { - "type": "string" + "type": "string", + "enum": [ + "create", + "select" + ], + "description": "Whether sessions created from this template target a new or existing user. Valid values: \"create\" (new user), \"select\" (existing user). When tenantFilter is \"AllTenants\", only \"create\" is permitted." + }, + "defaultUsageLocation": { + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "Default usage location for new users created from this template. LabelValue shape: { \"label\": \"Canada\", \"value\": \"CA\" }. The value must be an ISO 3166-1 alpha-2 country code. Accepts a plain country code string. PS1 unwraps .value with fallback to the raw string." }, "defaultUserName": { - "type": "string" + "type": "string", + "description": "Pre-fills the username prefix field when defaultUserAction is \"create\"." }, "generateTAPByDefault": { - "type": "boolean" - }, - "GUID": { - "type": "string" + "type": "boolean", + "description": "If true, JIT sessions created from this template will generate a Temporary Access Pass instead of a password. Only applies when defaultUserAction=\"create\"." }, "reasonTemplate": { - "type": "string" + "type": "string", + "description": "Default reason/justification text pre-filled when creating a JIT session from this template." }, "templateName": { - "type": "string" + "type": "string", + "description": "Unique name for this template within the target tenant. Required." }, "tenantFilter": { - "type": "string" + "type": "string", + "description": "Target tenant domain or GUID, or \"AllTenants\" for a global template. \"AllTenants\" templates cannot use defaultUserAction=\"select\"." } }, "required": [ @@ -6911,13 +8752,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -6994,13 +8835,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -7059,13 +8900,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -7144,11 +8985,11 @@ } } }, - "/api/EditRoomList": { + "/api/EditRetentionCompliancePolicy": { "post": { - "summary": "EditRoomList", + "summary": "EditRetentionCompliancePolicy", "tags": [ - "Email-Exchange > Resources" + "Security > Compliance-Retention" ], "security": [ { @@ -7167,21 +9008,39 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Exchange.Room.ReadWrite", - "x-cipp-warnings": [ - "customDataformatter is present on the CippFormPage for this endpoint. Form field names and/or types may be renamed or restructured before POST. Verify the transformer function in the frontend page file and correct param names/types accordingly." + "x-cipp-role": "Security.RetentionCompliancePolicy.ReadWrite", + "parameters": [ + { + "name": "Identity", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "State", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } ], "requestBody": { "required": true, @@ -7190,47 +9049,20 @@ "schema": { "type": "object", "properties": { - "tenantFilter": { + "Identity": { "type": "string" }, - "groupId": { + "Name": { "type": "string" }, - "displayName": { + "parameters": { "type": "string" }, - "description": { + "State": { "type": "string" }, - "mailNickname": { + "tenantFilter": { "type": "string" - }, - "AddMember": { - "type": "array", - "items": { - "type": "string" - } - }, - "RemoveMember": { - "type": "array", - "items": { - "type": "string" - } - }, - "AddOwner": { - "type": "array", - "items": { - "type": "string" - } - }, - "RemoveOwner": { - "type": "array", - "items": { - "type": "string" - } - }, - "allowExternal": { - "type": "boolean" } }, "required": [ @@ -7242,9 +9074,9 @@ } } }, - "/api/EditRoomMailbox": { + "/api/EditRoomList": { "post": { - "summary": "EditRoomMailbox", + "summary": "EditRoomList", "tags": [ "Email-Exchange > Resources" ], @@ -7265,13 +9097,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -7288,17 +9120,125 @@ "schema": { "type": "object", "properties": { - "tenantID": { + "tenantFilter": { "type": "string" }, - "roomId": { + "groupId": { + "type": "string", + "description": "Room list group ID (Guid or id). PS1 reads $RoomListObj.groupId." + }, + "displayName": { "type": "string" }, + "description": { + "type": "string" + }, + "mailNickname": { + "type": "string", + "description": "PS1 passes as 'Name' to Set-DistributionGroup." + }, + "AddMember": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Array of room email addresses (strings or {value: email} objects) to add." + }, + "RemoveMember": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Array of room email addresses or objects to remove." + }, + "AddOwner": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Array of user objects [{value: UPN, addedFields: {id}}] to add as owners." + }, + "RemoveOwner": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Array of user objects [{value: UPN, addedFields: {id}}] to remove from owners." + }, + "allowExternal": { + "type": "boolean", + "description": "Only sent if changed from original value (formatter deletes it if unchanged)." + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/EditRoomMailbox": { + "post": { + "summary": "EditRoomMailbox", + "tags": [ + "Email-Exchange > Resources" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Room.ReadWrite", + "x-cipp-warnings": [ + "customDataformatter is present on the CippFormPage for this endpoint. Form field names and/or types may be renamed or restructured before POST. Verify the transformer function in the frontend page file and correct param names/types accordingly." + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tenantID": { + "type": "string", + "description": "Tenant domain. Formatter reads from useSettings().currentTenant." + }, + "roomId": { + "type": "string", + "description": "Room mailbox identity (from router query). PS1 uses for all cmdlet Identity fields." + }, "hiddenFromAddressListsEnabled": { "type": "boolean" }, "capacity": { - "type": "integer" + "type": "integer", + "description": "PS1 passes as ResourceCapacity to Set-Mailbox." }, "building": { "type": "string" @@ -7310,7 +9250,8 @@ "type": "string" }, "street": { - "type": "string" + "type": "string", + "description": "PS1 passes to Set-Place as 'Street'." }, "city": { "type": "string" @@ -7322,7 +9263,8 @@ "type": "string" }, "countryOrRegion": { - "type": "string" + "type": "string", + "description": "2-letter country code; formatter unwraps autocomplete .value." }, "audioDeviceName": { "type": "string" @@ -7343,7 +9285,8 @@ "type": "array", "items": { "type": "string" - } + }, + "description": "Array of strings; formatter maps {label,value} objects to .value strings." }, "AllowConflicts": { "type": "boolean" @@ -7370,7 +9313,8 @@ "type": "boolean" }, "AutomateProcessing": { - "type": "string" + "type": "string", + "description": "Enum: None|AutoUpdate|AutoAccept. Formatter unwraps autocomplete .value." }, "AddOrganizerToSubject": { "type": "boolean" @@ -7382,7 +9326,8 @@ "type": "boolean" }, "WorkDays": { - "type": "string" + "type": "string", + "description": "Comma-separated string of work days. Formatter joins array of {value} objects." }, "WorkHoursStartTime": { "type": "string" @@ -7391,13 +9336,16 @@ "type": "string" }, "WorkingHoursTimeZone": { - "type": "string" + "type": "string", + "description": "Timezone standard name. Formatter unwraps autocomplete .value." }, "userPrincipalName": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: userPrincipalName" }, "DisplayName": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: DisplayName" } } } @@ -7429,13 +9377,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -7491,7 +9439,7 @@ }, "/api/EditSafeLinksPolicy": { "post": { - "summary": "EditSafeLinksPolicy", + "summary": "This function modifies an existing Safe Links policy and its associated rule.", "tags": [ "Security > Safe-Links-Policy" ], @@ -7512,19 +9460,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "description": "This function modifies an existing Safe Links policy and its associated rule.\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", + "description": "This function modifies an existing Safe Links policy and its associated rule.", "x-cipp-role": "Exchange.SpamFilter.ReadWrite", "parameters": [ { @@ -7535,7 +9483,8 @@ "in": "query", "required": false, "schema": { - "type": "string" + "type": "string", + "description": "Query parameter from PS1: PolicyName" } }, { @@ -7543,7 +9492,8 @@ "in": "query", "required": false, "schema": { - "type": "string" + "type": "string", + "description": "Query parameter from PS1: RuleName" } } ], @@ -7558,10 +9508,12 @@ "type": "string" }, "PolicyName": { - "type": "string" + "type": "string", + "description": "Identity of the existing SafeLinksPolicy to update" }, "RuleName": { - "type": "string" + "type": "string", + "description": "Identity of the existing SafeLinksRule to update" }, "EnableSafeLinksForEmail": { "type": "boolean" @@ -7594,7 +9546,8 @@ "type": "boolean" }, "AdminDisplayName": { - "type": "string" + "type": "string", + "description": "Policy description / display name" }, "CustomNotificationText": { "type": "string" @@ -7603,52 +9556,61 @@ "type": "array", "items": { "type": "string" - } + }, + "description": "Array of URL/domain/wildcard strings to exclude from rewriting" }, "Priority": { - "type": "integer" + "type": "integer", + "description": "Rule priority (lower = higher priority)" }, "Comments": { "type": "string" }, "State": { - "type": "boolean" + "type": "boolean", + "description": "true = enable rule, false = disable rule" }, "SentTo": { "type": "array", "items": { "type": "string" - } + }, + "description": "Array of recipient UPNs" }, "SentToMemberOf": { "type": "array", "items": { "type": "string" - } + }, + "description": "Array of group IDs" }, "RecipientDomainIs": { "type": "array", "items": { "type": "string" - } + }, + "description": "Array of domain strings" }, "ExceptIfSentTo": { "type": "array", "items": { "type": "string" - } + }, + "description": "Exception: array of recipient UPNs" }, "ExceptIfSentToMemberOf": { "type": "array", "items": { "type": "string" - } + }, + "description": "Exception: array of group IDs" }, "ExceptIfRecipientDomainIs": { "type": "array", "items": { "type": "string" - } + }, + "description": "Exception: array of domain strings" } }, "required": [ @@ -7662,7 +9624,7 @@ }, "/api/EditSafeLinksPolicyTemplate": { "post": { - "summary": "EditSafeLinksPolicyTemplate", + "summary": "This function updates an existing Safe Links policy template.", "tags": [ "Security > Safe-Links-Policy" ], @@ -7683,19 +9645,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "description": "This function updates an existing Safe Links policy template.\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", + "description": "This function updates an existing Safe Links policy template.", "x-cipp-role": "Exchange.SafeLinks.ReadWrite", "requestBody": { "required": true, @@ -7705,7 +9667,8 @@ "type": "object", "properties": { "ID": { - "type": "string" + "type": "string", + "description": "Template GUID. PS1 requires this to locate and update the template in Azure Table Storage." }, "TemplateName": { "type": "string" @@ -7765,7 +9728,8 @@ "type": "integer" }, "State": { - "type": "string" + "type": "string", + "description": "Formatter sends ruleValues.State ? 'Enabled' : 'Disabled' (string, not boolean) for template mode." }, "Comments": { "type": "string" @@ -7807,7 +9771,8 @@ } }, "tenantFilter": { - "type": "string" + "type": "string", + "description": "Not consumed by PS1 (AnyTenant endpoint) but included by formatter." } }, "required": [ @@ -7819,11 +9784,11 @@ } } }, - "/api/EditSpamFilter": { + "/api/EditSensitiveInfoType": { "post": { - "summary": "EditSpamFilter", + "summary": "EditSensitiveInfoType", "tags": [ - "Email-Exchange > Spamfilter" + "Security > Compliance-SIT" ], "security": [ { @@ -7842,22 +9807,22 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Exchange.SpamFilter.ReadWrite", + "x-cipp-role": "Security.SensitiveInfoType.ReadWrite", "parameters": [ { - "name": "name", + "name": "Identity", "in": "query", "required": false, "schema": { @@ -7875,86 +9840,33 @@ "schema": { "type": "object", "properties": { - "name": { - "type": "string" - }, - "state": { - "type": "string" - } - } - } - } - } - } - } - }, - "/api/EditTenant": { - "post": { - "summary": "EditTenant", - "tags": [ - "Tenant > Administration > Tenant" - ], - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StandardResults" - } - } - } - }, - "400": { - "description": "Bad request — missing required field or invalid input" - }, - "401": { - "description": "Unauthorized — invalid or missing bearer token" - }, - "403": { - "description": "Forbidden — caller lacks the required RBAC role" - }, - "500": { - "description": "Internal server error" - } - }, - "x-cipp-role": "Tenant.Config.ReadWrite", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "tenantGroups": { + "Identity": { "type": "string" }, - "tenantAlias": { + "Name": { "type": "string" }, - "customerId": { + "parameters": { "type": "string" }, - "GroupId": { + "tenantFilter": { "type": "string" } - } + }, + "required": [ + "tenantFilter" + ] } } } } } }, - "/api/EditTenantOffboardingDefaults": { + "/api/EditSensitivityLabel": { "post": { - "summary": "EditTenantOffboardingDefaults", + "summary": "EditSensitivityLabel", "tags": [ - "Tenant > Administration > Tenant" + "Security > Compliance-SensitivityLabel" ], "security": [ { @@ -7973,19 +9885,32 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Tenant.Config.ReadWrite", + "x-cipp-role": "Security.SensitivityLabel.ReadWrite", + "parameters": [ + { + "name": "Identity", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ], "requestBody": { "required": true, "content": { @@ -7993,33 +9918,33 @@ "schema": { "type": "object", "properties": { - "Alias": { + "Identity": { "type": "string" }, - "customerId": { + "Name": { "type": "string" }, - "defaultDomainName": { + "parameters": { "type": "string" }, - "Groups": { - "$ref": "#/components/schemas/LabelValue" - }, - "offboardingDefaults": { + "tenantFilter": { "type": "string" } - } + }, + "required": [ + "tenantFilter" + ] } } } } } }, - "/api/EditTransportRule": { + "/api/EditSpamFilter": { "post": { - "summary": "EditTransportRule", + "summary": "EditSpamFilter", "tags": [ - "Email-Exchange > Transport" + "Email-Exchange > Spamfilter" ], "security": [ { @@ -8038,13 +9963,302 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.SpamFilter.ReadWrite", + "parameters": [ + { + "name": "name", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "state": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/EditTenant": { + "post": { + "summary": "EditTenant", + "tags": [ + "Tenant > Administration > Tenant" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Config.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "selectedGroup": { + "$ref": "#/components/schemas/LabelValue" + }, + "tenantGroups": { + "type": "string", + "description": "(auto) API uses 'tenantGroups', frontend sends 'Groups' \u2014 likely the same field (similarity=0.67). Verify and correct type." + }, + "tenantAlias": { + "type": "string", + "description": "(auto) API uses 'tenantAlias', frontend sends 'Alias' \u2014 likely the same field (similarity=0.62). Verify and correct type." + }, + "customerId": { + "type": "string", + "description": "Body parameter from PS1: customerId" + }, + "GroupId": { + "type": "string", + "description": "Body parameter from PS1: GroupId" + } + } + } + } + } + } + } + }, + "/api/EditTenantAllowBlockListTemplate": { + "post": { + "summary": "EditTenantAllowBlockListTemplate", + "tags": [ + "Email-Exchange > Spamfilter" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.SpamFilter.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "entries": { + "type": "string" + }, + "GUID": { + "type": "string" + }, + "listMethod": { + "$ref": "#/components/schemas/LabelValue" + }, + "listType": { + "$ref": "#/components/schemas/LabelValue" + }, + "NoExpiration": { + "type": "boolean" + }, + "notes": { + "type": "string" + }, + "RemoveAfter": { + "type": "boolean" + }, + "templateName": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/EditTenantOffboardingDefaults": { + "post": { + "summary": "EditTenantOffboardingDefaults", + "tags": [ + "Tenant > Administration > Tenant" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Config.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "selectedGroup": { + "$ref": "#/components/schemas/LabelValue" + }, + "Alias": { + "type": "string", + "description": "Alias parameter" + }, + "customerId": { + "type": "string", + "description": "customerId parameter" + }, + "defaultDomainName": { + "type": "string", + "description": "defaultDomainName parameter" + }, + "Groups": { + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "Groups parameter" + }, + "offboardingDefaults": { + "type": "string", + "description": "offboardingDefaults parameter" + } + } + } + } + } + } + } + }, + "/api/EditTransportRule": { + "post": { + "summary": "EditTransportRule", + "tags": [ + "Email-Exchange > Transport" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -8121,13 +10335,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -8142,93 +10356,126 @@ "type": "object", "properties": { "userTemplate": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "(auto) from CippAddEditUser.jsx" }, "givenName": { - "type": "string" + "type": "string", + "description": "(auto) from CippAddEditUser.jsx" }, "surname": { - "type": "string" + "type": "string", + "description": "(auto) from CippAddEditUser.jsx" }, "Autopassword": { - "type": "boolean" + "type": "boolean", + "description": "(auto) from CippAddEditUser.jsx" }, "password": { "type": "string", - "format": "password" + "format": "password", + "description": "(auto) from CippAddEditUser.jsx" }, "MustChangePass": { - "type": "boolean" + "type": "boolean", + "description": "(auto) from CippAddEditUser.jsx" }, "usageLocation": { - "$ref": "#/components/schemas/LabelValue", + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "(auto) from CippAddEditUser.jsx", "type": "object", "properties": { "value": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: usageLocation.value" } } }, "sherweb": { - "type": "boolean" + "type": "boolean", + "description": "(auto) from CippAddEditUser.jsx" }, "removeLicenses": { - "type": "boolean" + "type": "boolean", + "description": "(auto) from CippAddEditUser.jsx" }, "jobTitle": { - "type": "string" + "type": "string", + "description": "(auto) from CippAddEditUser.jsx" }, "streetAddress": { - "type": "string" + "type": "string", + "description": "(auto) from CippAddEditUser.jsx" }, "city": { - "type": "string" + "type": "string", + "description": "(auto) from CippAddEditUser.jsx" }, "state": { - "type": "string" + "type": "string", + "description": "(auto) from CippAddEditUser.jsx" }, "postalCode": { - "type": "string" + "type": "string", + "description": "(auto) from CippAddEditUser.jsx" }, "country": { - "type": "string" + "type": "string", + "description": "(auto) from CippAddEditUser.jsx" }, "companyName": { - "type": "string" + "type": "string", + "description": "(auto) from CippAddEditUser.jsx" }, "department": { - "type": "string" + "type": "string", + "description": "(auto) from CippAddEditUser.jsx" }, "mobilePhone": { - "type": "string" + "type": "string", + "description": "(auto) from CippAddEditUser.jsx" }, "businessPhones": { - "type": "string" + "type": "string", + "description": "(auto) from CippAddEditUser.jsx" }, "otherMails": { - "type": "string" + "type": "string", + "description": "(auto) from CippAddEditUser.jsx" }, "AddToGroups": { "type": "array", "items": { "type": "string" - } + }, + "description": "(auto) from CippAddEditUser.jsx (array of LabelValue)" }, "RemoveFromGroups": { "type": "array", "items": { "type": "string" - } + }, + "description": "(auto) from CippAddEditUser.jsx (array of LabelValue)" }, "Scheduled": { "type": "object", "properties": { "enabled": { - "type": "boolean" + "type": "boolean", + "description": "(auto) from CippAddEditUser.jsx" }, "date": { "type": "string", - "format": "date-time" + "format": "date-time", + "description": "(auto) from CippAddEditUser.jsx" } } }, @@ -8236,25 +10483,35 @@ "type": "object", "properties": { "webhook": { - "type": "boolean" + "type": "boolean", + "description": "(auto) from CippAddEditUser.jsx" }, "email": { - "type": "boolean" + "type": "boolean", + "description": "(auto) from CippAddEditUser.jsx" }, "psa": { - "type": "boolean" + "type": "boolean", + "description": "(auto) from CippAddEditUser.jsx" } } }, "reference": { - "type": "string" + "type": "string", + "description": "(auto) from CippAddEditUser.jsx" }, "primDomain": { - "$ref": "#/components/schemas/LabelValue", + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "(auto) from CippAddEditUser.jsx", "type": "object", "properties": { "value": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: primDomain.value" } } }, @@ -8262,57 +10519,82 @@ "type": "array", "items": { "type": "string" - } + }, + "description": "(auto) from CippAddEditUser.jsx" }, "setManager": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "(auto) from CippAddEditUser.jsx" }, "setSponsor": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "(auto) from CippAddEditUser.jsx" }, "PostExecution": { - "type": "string" + "type": "string", + "description": "(auto) from API scan" }, "id": { - "type": "string" + "type": "string", + "description": "(auto) from API scan" }, "username": { - "type": "string" + "type": "string", + "description": "(auto) from API scan" }, "Domain": { - "type": "string" + "type": "string", + "description": "(auto) from API scan" }, "mailNickname": { - "type": "string" + "type": "string", + "description": "(auto) from API scan" }, "defaultAttributes": { - "type": "string" + "type": "string", + "description": "(auto) from API scan" }, "customData": { - "type": "string" + "type": "string", + "description": "(auto) from API scan" }, "tenantFilter": { - "type": "string" + "type": "string", + "description": "(auto) from API scan" }, "sherwebLicense": { "type": "object", + "description": "(auto) from API scan", "properties": { "value": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: sherwebLicense.value" } } }, "userPrincipalName": { - "type": "string" + "type": "string", + "description": "(auto) from API scan" }, "CopyFrom": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: CopyFrom" }, "DisplayName": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: DisplayName" }, "AddedAliases": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: AddedAliases" } }, "required": [ @@ -8347,13 +10629,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -8368,19 +10650,24 @@ "type": "object", "properties": { "AddedAliases": { - "type": "string" + "type": "string", + "description": "AddedAliases parameter" }, "id": { - "type": "string" + "type": "string", + "description": "id parameter" }, "MakePrimary": { - "type": "string" + "type": "string", + "description": "MakePrimary parameter" }, "RemovedAliases": { - "type": "string" + "type": "string", + "description": "RemovedAliases parameter" }, "tenantFilter": { - "type": "string" + "type": "string", + "description": "tenantFilter parameter" } }, "required": [ @@ -8415,13 +10702,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -8453,13 +10740,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -8524,13 +10811,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -8544,15 +10831,6 @@ "schema": { "type": "object", "properties": { - "email": { - "type": "string" - }, - "logsToInclude": { - "type": "string" - }, - "onePerTenant": { - "type": "string" - }, "sendEmailNow": { "type": "string" }, @@ -8562,19 +10840,19 @@ "sendWebhookNow": { "type": "string" }, - "Severity": { + "tenantFilter": { "type": "string" }, "text": { "type": "string" }, - "webhook": { - "type": "string" - }, "writeLog": { "type": "string" } - } + }, + "required": [ + "tenantFilter" + ] } } } @@ -8604,13 +10882,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -8618,7 +10896,7 @@ }, "x-cipp-role": "Tenant.Relationship.ReadWrite", "x-cipp-warnings": [ - "advancedMappings state is held in the component, not in RHF form values. In AddRoleAdvanced mode, only Action and Mappings are sent — all form field values are discarded." + "advancedMappings state is held in the component, not in RHF form values. In AddRoleAdvanced mode, only Action and Mappings are sent \u2014 all form field values are discarded." ], "parameters": [ { @@ -8626,7 +10904,8 @@ "in": "query", "required": false, "schema": { - "type": "string" + "type": "string", + "description": "Query parameter from PS1: Action" } } ], @@ -8647,25 +10926,31 @@ "type": "string" }, "Action": { - "type": "string" + "type": "string", + "description": "Dispatch action. Formatter sends 'AddRoleAdvanced' when advancedMode=true, or omits Action (defaults to 'AddRoleSimple') when false. PS1 defaults to 'AddRoleSimple' if not provided. Valid: AddRoleSimple, AddRoleAdvanced, ListGroups." }, "gdapRoles": { "type": "array", "items": { "type": "string" - } + }, + "description": "Array of {label, value} role objects. Used in AddRoleSimple mode. PS1 reads $Request.Body.gdapRoles." }, "customSuffix": { - "type": "string" + "type": "string", + "description": "Optional group name suffix. Used in AddRoleSimple mode." }, "templateId": { - "type": "string" + "type": "string", + "description": "If set, adds role mappings to this GDAP template. Used in AddRoleSimple mode." }, "mappings": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: mappings" }, "replace": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: replace" } } } @@ -8697,13 +10982,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -8768,13 +11053,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -8806,13 +11091,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -8865,13 +11150,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -8886,16 +11171,20 @@ "type": "object", "properties": { "IP": { - "type": "string" + "type": "string", + "description": "IP parameter" }, "ipAddress": { - "type": "string" + "type": "string", + "description": "ipAddress parameter" }, "State": { - "type": "string" + "type": "string", + "description": "State parameter" }, "tenantfilter": { - "type": "string" + "type": "string", + "description": "tenantfilter parameter" } }, "required": [ @@ -8930,13 +11219,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -8973,13 +11262,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -9021,6 +11310,9 @@ "IpRange": { "type": "string" }, + "MCPAllowed": { + "type": "string" + }, "RemoveAppReg": { "type": "string" }, @@ -9057,13 +11349,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -9105,13 +11397,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -9146,6 +11438,12 @@ "Action": { "type": "string" }, + "ApplicationManifest": { + "type": "string" + }, + "AppType": { + "type": "string" + }, "TemplateId": { "type": "string" }, @@ -9182,13 +11480,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -9220,13 +11518,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -9300,13 +11598,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -9338,13 +11636,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -9449,13 +11747,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -9514,13 +11812,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -9602,6 +11900,9 @@ "AssignTo": { "type": "string" }, + "excludeGroup": { + "type": "string" + }, "GroupIds": { "type": "string" }, @@ -9650,13 +11951,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -9736,13 +12037,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -9803,13 +12104,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -9822,7 +12123,8 @@ "in": "query", "required": false, "schema": { - "type": "string" + "type": "string", + "description": "Action parameter" } }, { @@ -9830,7 +12132,8 @@ "in": "query", "required": false, "schema": { - "type": "string" + "type": "string", + "description": "SearchId parameter" } }, { @@ -9845,22 +12148,28 @@ "type": "object", "properties": { "Action": { - "type": "string" + "type": "string", + "description": "Action parameter" }, "EndTime": { - "type": "string" + "type": "string", + "description": "EndTime parameter" }, "SearchId": { - "type": "string" + "type": "string", + "description": "SearchId parameter" }, "StartTime": { - "type": "string" + "type": "string", + "description": "StartTime parameter" }, "tenantFilter": { - "type": "string" + "type": "string", + "description": "tenantFilter parameter" }, "PSObject": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: PSObject" } }, "required": [ @@ -9895,13 +12204,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -9958,19 +12267,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "description": "This function is used to interact with Azure Tables. This is advanced functionality used for external integrations or SuperAdmin functionality.\n .FUNCTIONALITY\n Entrypoint\n .ROLE\n CIPP.SuperAdmin.ReadWrite\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)\n $AllowList = @(\n 'Add-AzDataTableEntity'\n 'Add-CIPPAzDataTableEntity'\n 'Update-AzDataTableEntity'\n 'Get-AzDataTableEntity'\n 'Get-CIPPAzDataTableEntity'\n 'Get-AzDataTable'\n 'New-AzDataTable'\n 'Remove-AzDataTableEntity'\n 'Remove-AzDataTable'\n )", + "description": "This function is used to interact with Azure Tables. This is advanced functionality used for external integrations or SuperAdmin functionality.", "x-cipp-role": "CIPP.SuperAdmin.ReadWrite", "requestBody": { "required": true, @@ -9980,16 +12289,20 @@ "type": "object", "properties": { "FunctionName": { - "type": "string" + "type": "string", + "description": "FunctionName parameter" }, "OffloadFunctions": { - "type": "boolean" + "type": "boolean", + "description": "OffloadFunctions parameter" }, "Parameters": { - "type": "string" + "type": "string", + "description": "Parameters parameter" }, "TableName": { - "type": "string" + "type": "string", + "description": "TableName parameter" } } } @@ -10021,13 +12334,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -10096,13 +12409,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -10158,13 +12471,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -10219,13 +12532,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -10257,13 +12570,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -10320,13 +12633,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -10414,13 +12727,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -10483,13 +12796,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -10539,13 +12852,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -10577,13 +12890,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -10660,13 +12973,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -10681,52 +12994,66 @@ "type": "object", "properties": { "tenantFilter": { - "type": "string" + "type": "string", + "description": "Formatter unwraps tenantFilter?.value || tenantFilter." }, "Users": { "type": "array", "items": { "type": "string" - } + }, + "description": "Array of user objects from CippFormUserSelector with addedFields {userPrincipalName, displayName}. PS1 reads Users.value and Users.addedFields.userPrincipalName." }, "PolicyId": { - "type": "string" + "type": "string", + "description": "CA policy ID. Formatter unwraps autocomplete .value." }, "StartDate": { - "type": "integer" + "type": "integer", + "description": "Unix timestamp (seconds) for start date/time. Formatter sends as 'StartDate' mapping form field 'startDate'." }, "EndDate": { - "type": "integer" + "type": "integer", + "description": "Unix timestamp (seconds) for end date/time. Formatter sends as 'EndDate' mapping form field 'endDate'." }, "vacation": { - "type": "boolean" + "type": "boolean", + "description": "Always set to true by this form (vacation mode path). PS1 checks if 'vacation' == 'true' (string comparison) to determine scheduling path." }, "postExecution": { "type": "array", "items": { "type": "string" - } + }, + "description": "Post-execution action objects. Not explicitly in this form but PS1 reads $Request.Body.postExecution." }, "reference": { - "type": "string" + "type": "string", + "description": "Not in this form but PS1 reads $Request.Body.reference." }, "UserID": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: UserID" }, "Username": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: Username" }, "ExclusionType": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: ExclusionType" }, "excludeLocationAuditAlerts": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: excludeLocationAuditAlerts" }, "value": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: value" }, "addedFields": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: addedFields" } }, "required": [ @@ -10761,13 +13088,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -10833,13 +13160,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -10869,9 +13196,9 @@ ] } }, - "/api/ExecCPVPermissions": { - "post": { - "summary": "ExecCPVPermissions", + "/api/ExecCIPPUsers": { + "delete": { + "summary": "ExecCIPPUsers", "tags": [ "CIPP > Settings" ], @@ -10892,22 +13219,22 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "CIPP.AppSettings.ReadWrite", + "x-cipp-role": "CIPP.SuperAdmin.ReadWrite", "parameters": [ { - "name": "ResetSP", + "name": "Action", "in": "query", "required": false, "schema": { @@ -10922,24 +13249,30 @@ "schema": { "type": "object", "properties": { - "tenantFilter": { + "Action": { + "type": "string" + }, + "Roles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LabelValue" + } + }, + "UPN": { "type": "string" } - }, - "required": [ - "tenantFilter" - ] + } } } } } } }, - "/api/ExecCPVRefresh": { - "get": { - "summary": "This endpoint is used to trigger a refresh of CPV for all tenants", + "/api/ExecCPVPermissions": { + "post": { + "summary": "ExecCPVPermissions", "tags": [ - "CIPP > Core" + "CIPP > Settings" ], "security": [ { @@ -10958,132 +13291,29 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "CIPP.Core.ReadWrite" - } - }, - "/api/ExecCSPLicense": { - "get": { - "summary": "ExecCSPLicense", - "tags": [ - "Uncategorized" - ], - "security": [ + "x-cipp-role": "CIPP.AppSettings.ReadWrite", + "parameters": [ { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StandardResults" - } - } - } - }, - "400": { - "description": "Bad request — missing required field or invalid input" - }, - "401": { - "description": "Unauthorized — invalid or missing bearer token" - }, - "403": { - "description": "Forbidden — caller lacks the required RBAC role" - }, - "500": { - "description": "Internal server error" - } - }, - "x-cipp-role": "Tenant.Directory.ReadWrite", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "Action": { - "type": "string" - }, - "Add": { - "type": "string" - }, - "iagree": { - "type": "boolean" - }, - "Quantity": { - "type": "number" - }, - "Remove": { - "type": "string" - }, - "SKU": { - "$ref": "#/components/schemas/LabelValue" - }, - "SubscriptionIds": { - "type": "string" - }, - "tenantFilter": { - "type": "string" - } - }, - "required": [ - "tenantFilter" - ] - } + "name": "ResetSP", + "in": "query", + "required": false, + "schema": { + "type": "string" } } - } - }, - "post": { - "summary": "ExecCSPLicense", - "tags": [ - "Uncategorized" ], - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StandardResults" - } - } - } - }, - "400": { - "description": "Bad request — missing required field or invalid input" - }, - "401": { - "description": "Unauthorized — invalid or missing bearer token" - }, - "403": { - "description": "Forbidden — caller lacks the required RBAC role" - }, - "500": { - "description": "Internal server error" - } - }, - "x-cipp-role": "Tenant.Directory.ReadWrite", "requestBody": { "required": true, "content": { @@ -11091,27 +13321,6 @@ "schema": { "type": "object", "properties": { - "Action": { - "type": "string" - }, - "Add": { - "type": "string" - }, - "iagree": { - "type": "boolean" - }, - "Quantity": { - "type": "number" - }, - "Remove": { - "type": "string" - }, - "SKU": { - "$ref": "#/components/schemas/LabelValue" - }, - "SubscriptionIds": { - "type": "string" - }, "tenantFilter": { "type": "string" } @@ -11125,9 +13334,9 @@ } } }, - "/api/ExecCippFunction": { - "post": { - "summary": "Execute a CIPPCore function", + "/api/ExecCPVRefresh": { + "get": { + "summary": "This endpoint is used to trigger a refresh of CPV for all tenants", "tags": [ "CIPP > Core" ], @@ -11148,45 +13357,26 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "description": "This function is used to execute a CIPPCore function from an HTTP request. This is advanced functionality used for external integrations or SuperAdmin functionality.\n .FUNCTIONALITY\n Entrypoint\n .ROLE\n CIPP.SuperAdmin.ReadWrite\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)\n $BlockList = @(\n 'Get-GraphToken'\n 'Get-GraphTokenFromCert'\n 'Get-ClassicAPIToken'\n )", - "x-cipp-role": "CIPP.SuperAdmin.ReadWrite", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "FunctionName": { - "type": "string" - }, - "Parameters": { - "type": "string" - } - } - } - } - } - } + "x-cipp-role": "CIPP.Core.ReadWrite" } }, - "/api/ExecCippLogsSas": { - "post": { - "summary": "Generate a read-only SAS token for the CippLogs table", + "/api/ExecCSPLicense": { + "get": { + "summary": "ExecCSPLicense", "tags": [ - "CIPP > Core" + "Uncategorized" ], "security": [ { @@ -11205,19 +13395,228 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "description": "Creates a long-lived, read-only SAS URL for the CippLogs Azure Storage table.\n .FUNCTIONALITY\n Entrypoint\n .ROLE\n CIPP.AppSettings.ReadWrite\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", + "x-cipp-role": "Tenant.Directory.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Action": { + "type": "string" + }, + "Add": { + "type": "string" + }, + "iagree": { + "type": "boolean" + }, + "Quantity": { + "type": "number" + }, + "Remove": { + "type": "string" + }, + "SKU": { + "$ref": "#/components/schemas/LabelValue" + }, + "SubscriptionIds": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + }, + "post": { + "summary": "ExecCSPLicense", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Directory.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Action": { + "type": "string" + }, + "Add": { + "type": "string" + }, + "iagree": { + "type": "boolean" + }, + "Quantity": { + "type": "number" + }, + "Remove": { + "type": "string" + }, + "SKU": { + "$ref": "#/components/schemas/LabelValue" + }, + "SubscriptionIds": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecCippFunction": { + "post": { + "summary": "Execute a CIPPCore function", + "tags": [ + "CIPP > Core" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "This function is used to execute a CIPPCore function from an HTTP request. This is advanced functionality used for external integrations or SuperAdmin functionality.", + "x-cipp-role": "CIPP.SuperAdmin.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "FunctionName": { + "type": "string" + }, + "Parameters": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecCippLogsSas": { + "post": { + "summary": "Generate a read-only SAS token for the CippLogs table", + "tags": [ + "CIPP > Core" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Creates a long-lived, read-only SAS URL for the CippLogs Azure Storage table.", "x-cipp-role": "CIPP.AppSettings.ReadWrite", "parameters": [ { @@ -11269,13 +13668,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -11291,6 +13690,14 @@ "type": "string" } }, + { + "name": "includeGlobal", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, { "name": "tenantId", "in": "query", @@ -11313,6 +13720,9 @@ "Description": { "type": "string" }, + "includeGlobal": { + "type": "string" + }, "RowKey": { "type": "string" }, @@ -11352,13 +13762,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -11426,13 +13836,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -11498,13 +13908,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -11575,19 +13985,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "description": "This function makes changes to a community repository in table storage\n .FUNCTIONALITY\n Entrypoint,AnyTenant\n .ROLE\n CIPP.Core.ReadWrite\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", + "description": "This function makes changes to a community repository in table storage", "x-cipp-role": "CIPP.Core.ReadWrite", "requestBody": { "required": true, @@ -11597,46 +14007,72 @@ "type": "object", "properties": { "Action": { - "type": "string" + "type": "string", + "description": "Action parameter" }, "Branch": { - "type": "string" + "type": "string", + "description": "Branch parameter" }, "Description": { - "type": "string" + "type": "string", + "description": "Description parameter" }, "FullName": { - "type": "string" + "type": "string", + "description": "FullName parameter" }, "GUID": { - "type": "string" + "type": "string", + "description": "GUID parameter" }, "Id": { - "type": "string" + "type": "string", + "description": "Id parameter" }, "includeforks": { - "type": "boolean" + "type": "boolean", + "description": "includeforks parameter" }, "Message": { - "type": "string" + "type": "string", + "description": "Message parameter" }, "orgName": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "orgName parameter" }, "Path": { - "type": "string" + "type": "string", + "description": "Path parameter" }, "policySource": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "policySource parameter" }, "Private": { - "type": "boolean" + "type": "boolean", + "description": "Private parameter" }, "repoName": { - "type": "string" + "type": "string", + "description": "repoName parameter" }, "searchTerm": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "searchTerm parameter" } } } @@ -11645,11 +14081,11 @@ } } }, - "/api/ExecConvertMailbox": { + "/api/ExecCompareIntunePolicy": { "post": { - "summary": "ExecConvertMailbox", + "summary": "ExecCompareIntunePolicy", "tags": [ - "Email-Exchange > Administration" + "Endpoint > MEM" ], "security": [ { @@ -11668,19 +14104,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "x-cipp-role": "Endpoint.MEM.Read", "requestBody": { "required": true, "content": { @@ -11688,30 +14124,24 @@ "schema": { "type": "object", "properties": { - "ID": { - "type": "string" - }, - "MailboxType": { + "sourceA": { "type": "string" }, - "tenantFilter": { + "sourceB": { "type": "string" } - }, - "required": [ - "tenantFilter" - ] + } } } } } } }, - "/api/ExecCopyForSent": { + "/api/ExecContainerManagement": { "post": { - "summary": "ExecCopyForSent", + "summary": "ExecContainerManagement", "tags": [ - "Email-Exchange > Administration" + "CIPP > Settings" ], "security": [ { @@ -11730,38 +14160,27 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "x-cipp-role": "CIPP.SuperAdmin.ReadWrite", "parameters": [ { - "name": "ID", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "messageCopyState", + "name": "Action", "in": "query", "required": false, "schema": { "type": "string" } - }, - { - "$ref": "#/components/parameters/tenantFilter" } ], "requestBody": { @@ -11771,30 +14190,33 @@ "schema": { "type": "object", "properties": { - "ID": { + "Action": { "type": "string" }, - "messageCopyState": { - "type": "string" + "AutoUpdate": { + "type": "boolean" }, - "tenantFilter": { - "type": "string" + "Channel": { + "$ref": "#/components/schemas/LabelValue" + }, + "CheckInterval": { + "$ref": "#/components/schemas/LabelValue" + }, + "CheckTime": { + "$ref": "#/components/schemas/LabelValue" } - }, - "required": [ - "tenantFilter" - ] + } } } } } } }, - "/api/ExecCreateAppTemplate": { + "/api/ExecConvertMailbox": { "post": { - "summary": "ExecCreateAppTemplate", + "summary": "ExecConvertMailbox", "tags": [ - "Tenant > Administration > Application Approval" + "Email-Exchange > Administration" ], "security": [ { @@ -11813,20 +14235,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Tenant.Application.ReadWrite", - "x-cipp-dynamic-options": true, + "x-cipp-role": "Exchange.Mailbox.ReadWrite", "requestBody": { "required": true, "content": { @@ -11834,24 +14255,18 @@ "schema": { "type": "object", "properties": { - "AppId": { - "type": "string" - }, - "DisplayName": { - "type": "string" - }, - "Overwrite": { + "ID": { "type": "string" }, - "TenantFilter": { + "MailboxType": { "type": "string" }, - "Type": { + "tenantFilter": { "type": "string" } }, "required": [ - "TenantFilter" + "tenantFilter" ] } } @@ -11859,11 +14274,11 @@ } } }, - "/api/ExecCreateDefaultGroups": { - "get": { - "summary": "Create default tenant groups", + "/api/ExecCopyForSent": { + "post": { + "summary": "ExecCopyForSent", "tags": [ - "CIPP > Settings" + "Email-Exchange > Administration" ], "security": [ { @@ -11882,19 +14297,227 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "description": "This function creates a set of default tenant groups that are commonly used\n .FUNCTIONALITY\n Entrypoint,AnyTenant\n .ROLE\n Tenant.Groups.ReadWrite\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", + "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "parameters": [ + { + "name": "ID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "messageCopyState", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ID": { + "type": "string" + }, + "messageCopyState": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecCreateAppTemplate": { + "post": { + "summary": "ExecCreateAppTemplate", + "tags": [ + "Tenant > Administration > Application Approval" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Application.ReadWrite", + "x-cipp-dynamic-options": true, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "AppId": { + "type": "string" + }, + "DisplayName": { + "type": "string" + }, + "Overwrite": { + "type": "string" + }, + "TenantFilter": { + "type": "string" + }, + "Type": { + "type": "string" + } + }, + "required": [ + "TenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecCreateCATemplate": { + "post": { + "summary": "ExecCreateCATemplate", + "tags": [ + "Tenant > Conditional" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.ConditionalAccess.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "displayName": { + "type": "string" + }, + "name": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecCreateDefaultGroups": { + "get": { + "summary": "Create default tenant groups", + "tags": [ + "CIPP > Settings" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "This function creates a set of default tenant groups that are commonly used", "x-cipp-role": "Tenant.Groups.ReadWrite" } }, @@ -11921,13 +14544,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -11942,7 +14565,8 @@ "type": "object", "properties": { "access_token": { - "type": "string" + "type": "string", + "description": "access_token parameter" } } } @@ -11974,13 +14598,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -12079,13 +14703,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -12185,13 +14809,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -12249,6 +14873,71 @@ } } }, + "/api/ExecCustomScript": { + "post": { + "summary": "ExecCustomScript", + "tags": [ + "Tools > Custom-Scripts" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Tests.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Parameters": { + "type": "string" + }, + "ScriptGuid": { + "type": "string" + }, + "TenantFilter": { + "type": "string" + }, + "TestParameters": { + "type": "string" + } + }, + "required": [ + "TenantFilter" + ] + } + } + } + } + } + }, "/api/ExecDeleteGDAPRelationship": { "post": { "summary": "ExecDeleteGDAPRelationship", @@ -12272,13 +14961,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -12335,13 +15024,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -12377,7 +15066,7 @@ }, "/api/ExecDeleteSafeLinksPolicy": { "post": { - "summary": "ExecDeleteSafeLinksPolicy", + "summary": "This function deletes a Safe Links rule and its associated policy.", "tags": [ "Security > Safe-Links-Policy" ], @@ -12398,19 +15087,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "description": "This function deletes a Safe Links rule and its associated policy.\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)\n $APIName = $Request.Params.CIPPEndpoint\n $Headers = $Request.Headers", + "description": "This function deletes a Safe Links rule and its associated policy.", "x-cipp-role": "Exchange.SpamFilter.ReadWrite", "parameters": [ { @@ -12459,11 +15148,11 @@ } } }, - "/api/ExecDeviceAction": { + "/api/ExecDeployAppTemplate": { "post": { - "summary": "ExecDeviceAction", + "summary": "ExecDeployAppTemplate", "tags": [ - "Endpoint > MEM" + "Endpoint > Applications" ], "security": [ { @@ -12482,13 +15171,75 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.Application.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "AssignTo": { + "type": "string" + }, + "customGroup": { + "type": "string" + }, + "selectedTenants": { + "type": "string" + }, + "templateId": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecDeviceAction": { + "post": { + "summary": "ExecDeviceAction", + "tags": [ + "Endpoint > MEM" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -12550,13 +15301,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -12630,13 +15381,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -12713,13 +15464,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -12775,13 +15526,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -12799,16 +15550,20 @@ "$ref": "#/components/schemas/LabelValue" }, "name": { - "type": "string" + "type": "string", + "description": "(auto) API uses 'name', frontend sends 'presetName' \u2014 likely the same field (similarity=0.57). Verify and correct type." }, "action": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: action" }, "GUID": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: GUID" }, "query": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: query" } } } @@ -12840,13 +15595,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -12923,13 +15678,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -13006,13 +15761,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -13102,13 +15857,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -13164,13 +15919,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -13220,13 +15975,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -13273,13 +16028,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -13307,11 +16062,11 @@ ] } }, - "/api/ExecEditCalendarPermissions": { + "/api/ExecEditCAPolicyFull": { "post": { - "summary": "ExecEditCalendarPermissions", + "summary": "ExecEditCAPolicyFull", "tags": [ - "Email-Exchange > Administration" + "Tenant > Conditional" ], "security": [ { @@ -13330,46 +16085,22 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "x-cipp-role": "Tenant.ConditionalAccess.ReadWrite", "parameters": [ { - "name": "CanViewPrivateItems", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "FolderName", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "Permissions", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "RemoveAccess", + "name": "PolicyId", "in": "query", "required": false, "schema": { @@ -13378,22 +16109,6 @@ }, { "$ref": "#/components/parameters/tenantFilter" - }, - { - "name": "userid", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "UserToGetPermissions", - "in": "query", - "required": false, - "schema": { - "type": "string" - } } ], "requestBody": { @@ -13403,26 +16118,14 @@ "schema": { "type": "object", "properties": { - "CanViewPrivateItems": { - "type": "string" - }, - "FolderName": { + "PolicyBody": { "type": "string" }, - "Permissions": { - "type": "string" - }, - "RemoveAccess": { + "PolicyId": { "type": "string" }, "tenantFilter": { "type": "string" - }, - "userid": { - "type": "string" - }, - "UserToGetPermissions": { - "type": "string" } }, "required": [ @@ -13434,9 +16137,9 @@ } } }, - "/api/ExecEditMailboxPermissions": { + "/api/ExecEditCalendarPermissions": { "post": { - "summary": "ExecEditMailboxPermissions", + "summary": "ExecEditCalendarPermissions", "tags": [ "Email-Exchange > Administration" ], @@ -13457,103 +16160,65 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, "x-cipp-role": "Exchange.Mailbox.ReadWrite", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "AddFullAccess": { - "type": "string" - }, - "AddFullAccessNoAutoMap": { - "type": "string" - }, - "AddSendAs": { - "type": "string" - }, - "AddSendOnBehalf": { - "type": "string" - }, - "RemoveFullAccess": { - "type": "string" - }, - "RemoveSendAs": { - "type": "string" - }, - "RemoveSendOnBehalf": { - "type": "string" - }, - "tenantfilter": { - "type": "string" - }, - "userID": { - "type": "string" - } - }, - "required": [ - "tenantfilter" - ] - } + "parameters": [ + { + "name": "CanViewPrivateItems", + "in": "query", + "required": false, + "schema": { + "type": "string" } - } - } - } - }, - "/api/ExecEditTemplate": { - "post": { - "summary": "ExecEditTemplate", - "tags": [ - "CIPP > Core" - ], - "security": [ + }, { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StandardResults" - } - } + "name": "FolderName", + "in": "query", + "required": false, + "schema": { + "type": "string" } }, - "400": { - "description": "Bad request — missing required field or invalid input" + { + "name": "Permissions", + "in": "query", + "required": false, + "schema": { + "type": "string" + } }, - "401": { - "description": "Unauthorized — invalid or missing bearer token" + { + "name": "RemoveAccess", + "in": "query", + "required": false, + "schema": { + "type": "string" + } }, - "403": { - "description": "Forbidden — caller lacks the required RBAC role" + { + "$ref": "#/components/parameters/tenantFilter" }, - "500": { - "description": "Internal server error" - } - }, - "x-cipp-role": "CIPP.Core.ReadWrite", - "x-cipp-dynamic-options": true, - "parameters": [ { - "name": "Type", + "name": "userid", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "UserToGetPermissions", "in": "query", "required": false, "schema": { @@ -13568,37 +16233,40 @@ "schema": { "type": "object", "properties": { - "description": { + "CanViewPrivateItems": { "type": "string" }, - "displayName": { + "FolderName": { "type": "string" }, - "GUID": { + "Permissions": { "type": "string" }, - "id": { + "RemoveAccess": { "type": "string" }, - "name": { + "tenantFilter": { "type": "string" }, - "parsedRAWJson": { + "userid": { "type": "string" }, - "Type": { + "UserToGetPermissions": { "type": "string" } - } + }, + "required": [ + "tenantFilter" + ] } } } } } }, - "/api/ExecEmailForward": { + "/api/ExecEditMailboxPermissions": { "post": { - "summary": "ExecEmailForward", + "summary": "ExecEditMailboxPermissions", "tags": [ "Email-Exchange > Administration" ], @@ -13619,13 +16287,175 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "AddFullAccess": { + "type": "string" + }, + "AddFullAccessNoAutoMap": { + "type": "string" + }, + "AddSendAs": { + "type": "string" + }, + "AddSendOnBehalf": { + "type": "string" + }, + "RemoveFullAccess": { + "type": "string" + }, + "RemoveSendAs": { + "type": "string" + }, + "RemoveSendOnBehalf": { + "type": "string" + }, + "tenantfilter": { + "type": "string" + }, + "userID": { + "type": "string" + } + }, + "required": [ + "tenantfilter" + ] + } + } + } + } + } + }, + "/api/ExecEditTemplate": { + "post": { + "summary": "ExecEditTemplate", + "tags": [ + "CIPP > Core" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Core.ReadWrite", + "x-cipp-dynamic-options": true, + "parameters": [ + { + "name": "Type", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "GUID": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "parsedRAWJson": { + "type": "string" + }, + "Type": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ExecEmailForward": { + "post": { + "summary": "ExecEmailForward", + "tags": [ + "Email-Exchange > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -13690,13 +16520,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -13773,13 +16603,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -13835,13 +16665,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -13898,13 +16728,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -13960,13 +16790,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -14027,9 +16857,9 @@ } } }, - "/api/ExecExtensionMapping": { + "/api/ExecExtensionClearHIBPKey": { "get": { - "summary": "ExecExtensionMapping", + "summary": "ExecExtensionClearHIBPKey", "tags": [ "CIPP > Extensions" ], @@ -14050,77 +16880,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" - }, - "500": { - "description": "Internal server error" - } - }, - "x-cipp-role": "CIPP.Extension.ReadWrite", - "parameters": [ - { - "name": "AddMapping", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "AutoMapping", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "List", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/ExecExtensionNinjaOneQueue": { - "get": { - "summary": "ExecExtensionNinjaOneQueue", - "tags": [ - "Uncategorized" - ], - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StandardResults" - } - } - } - }, - "400": { - "description": "Bad request — missing required field or invalid input" - }, - "401": { - "description": "Unauthorized — invalid or missing bearer token" - }, - "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -14129,9 +16895,9 @@ "x-cipp-role": "CIPP.Extension.ReadWrite" } }, - "/api/ExecExtensionSync": { + "/api/ExecExtensionMapping": { "get": { - "summary": "ExecExtensionSync", + "summary": "ExecExtensionMapping", "tags": [ "CIPP > Extensions" ], @@ -14152,13 +16918,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -14167,7 +16933,7 @@ "x-cipp-role": "CIPP.Extension.ReadWrite", "parameters": [ { - "name": "Extension", + "name": "AddMapping", "in": "query", "required": false, "schema": { @@ -14175,7 +16941,15 @@ } }, { - "name": "TenantID", + "name": "AutoMapping", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "List", "in": "query", "required": false, "schema": { @@ -14185,11 +16959,11 @@ ] } }, - "/api/ExecExtensionTest": { + "/api/ExecExtensionNinjaOneQueue": { "get": { - "summary": "ExecExtensionTest", + "summary": "ExecExtensionNinjaOneQueue", "tags": [ - "CIPP > Extensions" + "Uncategorized" ], "security": [ { @@ -14208,34 +16982,24 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "CIPP.Extension.Read", - "parameters": [ - { - "name": "extensionName", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - } - ] + "x-cipp-role": "CIPP.Extension.ReadWrite" } }, - "/api/ExecExtensionsConfig": { - "post": { - "summary": "ExecExtensionsConfig", + "/api/ExecExtensionSync": { + "get": { + "summary": "ExecExtensionSync", "tags": [ "CIPP > Extensions" ], @@ -14256,13 +17020,117 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Extension.ReadWrite", + "parameters": [ + { + "name": "Extension", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "TenantID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/ExecExtensionTest": { + "get": { + "summary": "ExecExtensionTest", + "tags": [ + "CIPP > Extensions" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Extension.Read", + "parameters": [ + { + "name": "extensionName", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/ExecExtensionsConfig": { + "post": { + "summary": "ExecExtensionsConfig", + "tags": [ + "CIPP > Extensions" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -14278,13 +17146,16 @@ "type": "object", "properties": { "Hudu": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: Hudu" }, "NinjaOne": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: NinjaOne" }, "PSObject": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: PSObject" } } } @@ -14316,13 +17187,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -14375,13 +17246,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -14452,13 +17323,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -14471,7 +17342,8 @@ "in": "query", "required": false, "schema": { - "type": "string" + "type": "string", + "description": "Action parameter" } } ], @@ -14483,25 +17355,40 @@ "type": "object", "properties": { "Action": { - "type": "string" + "type": "string", + "description": "Action parameter" }, "gdapTemplate": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "gdapTemplate parameter" }, "inviteCount": { - "type": "number" + "type": "number", + "description": "inviteCount parameter" }, "InviteId": { - "type": "string" + "type": "string", + "description": "InviteId parameter" }, "Reference": { - "type": "string" + "type": "string", + "description": "Reference parameter" }, "roleMappings": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "roleMappings parameter" }, "roleDefinitionId": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: roleDefinitionId" } } } @@ -14533,13 +17420,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -14571,13 +17458,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -14634,13 +17521,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -14654,7 +17541,8 @@ "in": "query", "required": false, "schema": { - "type": "string" + "type": "string", + "description": "Action parameter" } }, { @@ -14662,7 +17550,8 @@ "in": "query", "required": false, "schema": { - "type": "string" + "type": "string", + "description": "TemplateId parameter" } } ], @@ -14674,25 +17563,40 @@ "type": "object", "properties": { "gdapTemplate": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "gdapTemplate parameter" }, "GroupId": { - "type": "string" + "type": "string", + "description": "GroupId parameter" }, "inviteCount": { - "type": "number" + "type": "number", + "description": "inviteCount parameter" }, "OriginalTemplateId": { - "type": "string" + "type": "string", + "description": "OriginalTemplateId parameter" }, "Reference": { - "type": "string" + "type": "string", + "description": "Reference parameter" }, "RoleMappings": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "RoleMappings parameter" }, "TemplateId": { - "type": "string" + "type": "string", + "description": "TemplateId parameter" } } } @@ -14724,13 +17628,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -14753,6 +17657,90 @@ ] } }, + "/api/ExecGenerateReportBuilderReport": { + "delete": { + "summary": "ExecGenerateReportBuilderReport", + "tags": [ + "Tools" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Core.ReadWrite", + "parameters": [ + { + "name": "TemplateName", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Action": { + "type": "string" + }, + "Blocks": { + "type": "string" + }, + "ReportGUID": { + "type": "string" + }, + "TemplateGUID": { + "type": "string" + }, + "TemplateName": { + "type": "string" + }, + "TenantFilter": { + "type": "string" + } + }, + "required": [ + "TenantFilter" + ] + } + } + } + } + } + }, "/api/ExecGeoIPLookup": { "post": { "summary": "ExecGeoIPLookup", @@ -14776,13 +17764,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -14839,13 +17827,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -14898,13 +17886,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -14973,19 +17961,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "description": "Call GitHub API\n .ROLE\n CIPP.Extension.ReadWrite\n .FUNCTIONALITY\n Entrypoint,AnyTenant\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", + "description": "Call GitHub API", "x-cipp-role": "CIPP.Extension.ReadWrite", "parameters": [ { @@ -14993,7 +17981,8 @@ "in": "query", "required": false, "schema": { - "type": "string" + "type": "string", + "description": "Action parameter" } } ], @@ -15005,28 +17994,48 @@ "type": "object", "properties": { "Action": { - "type": "string" + "type": "string", + "description": "Action parameter" }, "Description": { - "type": "string" + "type": "string", + "description": "Description parameter" }, "includeforks": { - "type": "boolean" + "type": "boolean", + "description": "includeforks parameter" }, "orgName": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "orgName parameter" }, "policySource": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "policySource parameter" }, "Private": { - "type": "boolean" + "type": "boolean", + "description": "Private parameter" }, "repoName": { - "type": "string" + "type": "string", + "description": "repoName parameter" }, "searchTerm": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "searchTerm parameter" } } } @@ -15058,13 +18067,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -15079,37 +18088,56 @@ "type": "object", "properties": { "action": { - "type": "string" + "type": "string", + "description": "action parameter" }, "AsApp": { - "type": "boolean" + "type": "boolean", + "description": "AsApp parameter" }, "endpoint": { - "type": "string" + "type": "string", + "description": "endpoint parameter" }, "IsShared": { - "type": "boolean" + "type": "boolean", + "description": "IsShared parameter" }, "name": { - "type": "string" + "type": "string", + "description": "name parameter" }, "NoPagination": { - "type": "boolean" + "type": "boolean", + "description": "NoPagination parameter" }, "preset": { - "type": "string" + "type": "string", + "description": "preset parameter" }, "reportTemplate": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "reportTemplate parameter" }, "ReverseTenantLookup": { - "type": "boolean" + "type": "boolean", + "description": "ReverseTenantLookup parameter" }, "ReverseTenantLookupProperty": { - "type": "string" + "type": "string", + "description": "ReverseTenantLookupProperty parameter" }, "version": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "version parameter" } } } @@ -15118,11 +18146,11 @@ } } }, - "/api/ExecGroupsDelete": { - "post": { - "summary": "ExecGroupsDelete", + "/api/ExecGraphRequestProfile": { + "get": { + "summary": "ExecGraphRequestProfile", "tags": [ - "Email-Exchange > Administration" + "CIPP > Core" ], "security": [ { @@ -15141,13 +18169,96 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Core.Read", + "parameters": [ + { + "name": "Endpoint", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "manualPagination", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Mode", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Reset", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "Version", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/ExecGroupsDelete": { + "post": { + "summary": "ExecGroupsDelete", + "tags": [ + "Email-Exchange > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -15235,13 +18346,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -15329,13 +18440,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -15423,13 +18534,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -15443,17 +18554,39 @@ "schema": { "type": "object", "properties": { - "displayName": { + "Action": { "type": "string" }, - "password": { + "BillingPolicyId": { "type": "string" }, - "primarySMTPAddress": { + "domain": { "type": "string" }, - "TenantFilter": { + "Identity": { + "type": "string" + }, + "ReplyTo": { + "type": "string" + }, + "username": { "type": "string" + }, + "displayName": { + "type": "string", + "description": "displayName parameter" + }, + "password": { + "type": "string", + "description": "password parameter" + }, + "primarySMTPAddress": { + "type": "string", + "description": "primarySMTPAddress parameter" + }, + "TenantFilter": { + "type": "string", + "description": "TenantFilter parameter" } }, "required": [ @@ -15488,13 +18621,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -15571,13 +18704,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -15609,7 +18742,7 @@ }, "/api/ExecJITAdmin": { "post": { - "summary": "ExecJITAdmin", + "summary": "Just-in-time admin management API endpoint.", "tags": [ "Identity > Administration > Users" ], @@ -15630,19 +18763,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "description": "Just-in-time admin management API endpoint. This function can create users, add roles, remove roles, delete, or disable a user.\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", + "description": "Just-in-time admin management API endpoint. This function can create users, add roles, remove roles, delete, or disable a user.", "x-cipp-role": "Identity.Role.ReadWrite", "requestBody": { "required": true, @@ -15651,9 +18784,6 @@ "schema": { "type": "object", "properties": { - "GroupMemberships": { - "type": "string" - }, "useGroups": { "type": "boolean" }, @@ -15661,58 +18791,113 @@ "type": "boolean" }, "AdminRoles": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "Array of admin roles to assign. LabelValue shape: { \"label\": \"Role Display Name\", \"value\": \"\" }. The value must be the role's ObjectId (GUID). Retrieve available roles from GET /api/ListRoles?TenantFilter=." }, "Domain": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "Domain for the new user's UPN. Required when userAction=\"create\". Accepts a LabelValue object ({ \"label\": \"contoso.com\", \"value\": \"contoso.com\" }) or a plain domain string. PS1 unwraps .value with fallback to the raw string." }, "EndDate": { "type": "string", - "format": "date-time" + "format": "date-time", + "description": "Unix timestamp (seconds) for when the JIT access expires. The scheduler removes/disables the user at this time per ExpireAction." }, "existingUser": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "Target user when userAction=\"select\". LabelValue shape: { \"label\": \"User Display Name\", \"value\": \"\" }. If value is a GUID (ObjectId), the PS1 resolves it to a UPN automatically via Graph. Required when userAction=\"select\"." }, "ExpireAction": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "Action to perform when JIT access expires. LabelValue shape: { \"label\": \"...\", \"value\": \"\" }. Valid values: \"DisableUser\" (disable the account), \"DeleteUser\" (permanently delete), \"RemoveRoles\" (userAction=select only), \"RemoveGroups\" (userAction=select only), \"RemoveRolesAndGroups\" (userAction=select only). Accepts a plain string instead of a LabelValue object." }, "FirstName": { - "type": "string" + "type": "string", + "description": "First name for the new user. Required when userAction=\"create\"." + }, + "GroupMemberships": { + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "Array of groups to add the user to. LabelValue shape: { \"label\": \"Group Display Name\", \"value\": \"\" }. The value must be the group's ObjectId (GUID). Used when ExpireAction includes group removal." }, "jitAdminTemplate": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "Template to pre-populate defaults from. LabelValue shape: { \"label\": \"Template Name\", \"value\": \"\" }. Retrieve available templates from GET /api/ListJITAdminTemplates?TenantFilter=." }, "LastName": { - "type": "string" + "type": "string", + "description": "Last name for the new user. Required when userAction=\"create\"." }, "PostExecution": { "type": "array", "items": { "type": "string" - } + }, + "description": "Notification actions to trigger after JIT access is granted. Array of strings. Valid values: \"Webhook\", \"email\", \"PSA\"." + }, + "reason": { + "type": "string", + "description": "Justification text for why JIT access is being granted. Stored on the JIT admin record." }, "StartDate": { "type": "string", - "format": "date-time" + "format": "date-time", + "description": "Unix timestamp (seconds) for when JIT access begins. Defaults to immediate if omitted." + }, + "tapLifetimeInMinutes": { + "type": "string", + "description": "Lifetime of the Temporary Access Pass in minutes. Only used when UseTAP=true. Must be within the tenant's TAP policy limits. Omit to use the tenant default." }, "tenantFilter": { - "type": "string" + "type": "string", + "description": "Target tenant domain or GUID. Required." }, "userAction": { "type": "string", "enum": [ "create", "select" - ] + ], + "description": "Whether to create a new user or target an existing one. \"create\" requires FirstName, LastName, username, Domain. \"select\" requires existingUser." }, - "UseTAP": { - "type": "boolean" + "usageLocation": { + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "Usage location for the new user (required by Microsoft for license assignment). LabelValue shape: { \"label\": \"Country Name\", \"value\": \"\" } \u2014 e.g. { \"label\": \"Canada\", \"value\": \"CA\" }. Accepts a plain country code string. Only used when userAction=\"create\"." }, - "Username": { - "type": "string" + "username": { + "type": "string", + "description": "Username prefix (without domain) for the new user. Combined with Domain to form the UPN. Required when userAction=\"create\"." }, - "Reason": { - "type": "string" + "UseTAP": { + "type": "boolean", + "description": "If true, generates a Temporary Access Pass instead of a password for the new user. Only applicable when userAction=\"create\"." } }, "required": [ @@ -15747,13 +18932,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -15766,7 +18951,8 @@ "in": "query", "required": false, "schema": { - "type": "string" + "type": "string", + "description": "GET requests use Action=Get query param. POST uses body." } } ], @@ -15778,10 +18964,12 @@ "type": "object", "properties": { "Action": { - "type": "string" + "type": "string", + "description": "Always 'Set' when posting from this form. Also accepted via query string. PS1 switches on this value." }, "MaxDuration": { - "type": "string" + "type": "string", + "description": "ISO 8601 duration string (e.g. 'PT4H', 'P1D', 'P28D') or null/empty for no limit. Formatter sends MaxDuration?.value ?? null \u2014 unwraps from autocomplete object." } } } @@ -15813,13 +19001,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -15866,13 +19054,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -15902,13 +19090,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -15940,13 +19128,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -16007,13 +19195,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -16070,13 +19258,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -16118,13 +19306,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -16201,13 +19389,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -16220,7 +19408,8 @@ "in": "query", "required": false, "schema": { - "type": "string" + "type": "string", + "description": "Query param for Remove|Resume|Suspend actions on existing requests. Not sent by this form." } }, { @@ -16228,7 +19417,8 @@ "in": "query", "required": false, "schema": { - "type": "string" + "type": "string", + "description": "Existing restore request identity for Remove/Resume/Suspend. Not sent by this form." } }, { @@ -16243,16 +19433,20 @@ "type": "object", "properties": { "TenantFilter": { - "type": "string" + "type": "string", + "description": "Formatter sends as 'TenantFilter' (capital T). PS1 reads $Request.Body.TenantFilter." }, "RequestName": { - "type": "string" + "type": "string", + "description": "Auto-generated in form as 'Restore {source} to {target} ({uuid})'." }, "SourceMailbox": { - "type": "string" + "type": "string", + "description": "Formatter sends ExchangeGuid from addedFields if present, otherwise UPN. PS1 reads .value ?? raw value. NOTE: formatter pre-resolves to ExchangeGuid string, so PS1 receives the guid directly." }, "TargetMailbox": { - "type": "string" + "type": "string", + "description": "Same as SourceMailbox \u2014 ExchangeGuid or UPN string." }, "BadItemLimit": { "type": "integer" @@ -16264,19 +19458,22 @@ "type": "boolean" }, "AssociatedMessagesCopyOption": { - "type": "string" + "type": "string", + "description": "Enum: DoNotCopy|MapByMessageClass|Copy. PS1 reads .value from the object." }, "ExcludeFolders": { "type": "array", "items": { "type": "string" - } + }, + "description": "Array of folder specifiers e.g. '#Inbox#'. PS1 reads .value from array items." }, "IncludeFolders": { "type": "array", "items": { "type": "string" - } + }, + "description": "Array of folder specifiers. PS1 reads .value from array items." }, "BatchName": { "type": "string" @@ -16285,7 +19482,8 @@ "type": "integer" }, "ConflictResolutionOption": { - "type": "string" + "type": "string", + "description": "Enum: ForceCopy|KeepAll|KeepLatestItem|KeepSourceItem|KeepTargetItem|UpdateFromSource. PS1 reads .value." }, "SourceRootFolder": { "type": "string" @@ -16294,7 +19492,8 @@ "type": "string" }, "TargetType": { - "type": "string" + "type": "string", + "description": "Enum: Archive|MailboxLocation|Primary. PS1 reads .value." }, "ExcludeDumpster": { "type": "boolean" @@ -16306,10 +19505,12 @@ "type": "boolean" }, "Action": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: Action" }, "Identity": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: Identity" } }, "required": [ @@ -16344,13 +19545,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -16377,6 +19578,80 @@ ] } }, + "/api/ExecManageAppCredentials": { + "patch": { + "summary": "ExecManageAppCredentials", + "tags": [ + "Tenant > Administration > Application Approval" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Application.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Action": { + "type": "string" + }, + "AppId": { + "type": "string" + }, + "AppType": { + "type": "string" + }, + "CredentialType": { + "type": "string" + }, + "Id": { + "type": "string" + }, + "KeyId": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, "/api/ExecManageRetentionPolicies": { "delete": { "summary": "ExecManageRetentionPolicies", @@ -16400,13 +19675,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -16425,7 +19700,8 @@ "in": "query", "required": false, "schema": { - "type": "string" + "type": "string", + "description": "Used for GET (single policy) requests." } } ], @@ -16443,19 +19719,22 @@ "type": "array", "items": { "type": "string" - } + }, + "description": "Array of policy objects to create: [{Name: string, RetentionPolicyTagLinks: string[]}]. Sent when isEdit=false." }, "ModifyPolicies": { "type": "array", "items": { "type": "string" - } + }, + "description": "Array of policy modification objects: [{Identity: string, Name: string, RetentionPolicyTagLinks: string[]}]. Sent when isEdit=true." }, "DeletePolicies": { "type": "array", "items": { "type": "string" - } + }, + "description": "Array of policy identity strings to delete. Not sent by this form but accepted by PS1." } }, "required": [ @@ -16490,13 +19769,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -16515,7 +19794,8 @@ "in": "query", "required": false, "schema": { - "type": "string" + "type": "string", + "description": "Used for GET (single tag) requests." } } ], @@ -16536,19 +19816,22 @@ "type": "array", "items": { "type": "string" - } + }, + "description": "Array of tag objects to create: [{Name, Type, Comment, RetentionAction, AgeLimitForRetention, RetentionEnabled, LocalizedComment, LocalizedRetentionPolicyTagName}]. Sent when isEdit=false." }, "ModifyTags": { "type": "array", "items": { "type": "string" - } + }, + "description": "Array of tag modification objects: [{Identity: original name, Name?, Comment?, RetentionAction?, AgeLimitForRetention?, RetentionEnabled?, LocalizedComment?, LocalizedRetentionPolicyTagName?}]. Sent when isEdit=true." }, "DeleteTags": { "type": "array", "items": { "type": "string" - } + }, + "description": "Array of tag identity strings to delete. Not sent by this form but accepted by PS1." } }, "required": [ @@ -16560,6 +19843,77 @@ } } }, + "/api/ExecMcp": { + "post": { + "summary": "Model Context Protocol (MCP) server endpoint for CIPP.", + "tags": [ + "CIPP > MCP" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "A Streamable-HTTP MCP server running in JSON response mode (no SSE). It exposes\n CIPP's read-only API surface as MCP tools, projected at runtime from openapi.json.\n\n Every 'tools/call' is re-dispatched through New-CippCoreRequest using the caller's\n own principal headers, so the standard RBAC and tenant scoping in Test-CIPPAccess\n is enforced for each tool exactly as it would be for a normal API request. This\n endpoint's own role (CIPP.Core.Read) is only the floor required to use MCP at all.", + "x-cipp-role": "CIPP.Core.Read", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "method": { + "type": "string" + }, + "params": { + "type": "object", + "properties": { + "arguments": { + "type": "string" + }, + "name": { + "type": "string" + }, + "protocolVersion": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, "/api/ExecMdoAlertsList": { "get": { "summary": "ExecMdoAlertsList", @@ -16583,13 +19937,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -16626,13 +19980,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -16688,13 +20042,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -16750,13 +20104,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -16845,13 +20199,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -16918,7 +20272,7 @@ }, "/api/ExecNewSafeLinksPolicy": { "post": { - "summary": "ExecNewSafeLinksPolicy", + "summary": "This function creates a new Safe Links policy and an associated rule.", "tags": [ "Security > Safe-Links-Policy" ], @@ -16939,19 +20293,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "description": "This function creates a new Safe Links policy and an associated rule.\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", + "description": "This function creates a new Safe Links policy and an associated rule.", "x-cipp-role": "Exchange.SpamFilter.ReadWrite", "parameters": [ { @@ -16969,10 +20323,12 @@ "type": "string" }, "PolicyName": { - "type": "string" + "type": "string", + "description": "Name for the new SafeLinksPolicy; must be unique in the tenant" }, "RuleName": { - "type": "string" + "type": "string", + "description": "Name for the new SafeLinksRule; auto-set to '{PolicyName}_Rule' by frontend; must be unique" }, "EnableSafeLinksForEmail": { "type": "boolean" @@ -17005,7 +20361,8 @@ "type": "boolean" }, "AdminDisplayName": { - "type": "string" + "type": "string", + "description": "Policy description / display name" }, "CustomNotificationText": { "type": "string" @@ -17014,55 +20371,65 @@ "type": "array", "items": { "type": "string" - } + }, + "description": "Array of URL/domain/wildcard strings to exclude from rewriting" }, "Priority": { - "type": "integer" + "type": "integer", + "description": "Rule priority (lower = higher priority)" }, "Comments": { "type": "string" }, "State": { - "type": "boolean" + "type": "boolean", + "description": "true = enable rule on creation, false = create disabled" }, "SentTo": { "type": "array", "items": { "type": "string" - } + }, + "description": "Array of recipient UPNs" }, "SentToMemberOf": { "type": "array", "items": { "type": "string" - } + }, + "description": "Array of group IDs" }, "RecipientDomainIs": { "type": "array", "items": { "type": "string" - } + }, + "description": "Array of domain strings" }, "ExceptIfSentTo": { "type": "array", "items": { "type": "string" - } + }, + "description": "Exception: array of recipient UPNs" }, "ExceptIfSentToMemberOf": { "type": "array", "items": { "type": "string" - } + }, + "description": "Exception: array of group IDs" }, "ExceptIfRecipientDomainIs": { "type": "array", "items": { "type": "string" - } + }, + "description": "Exception: array of domain strings" }, "Count": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: Count" } }, "required": [ @@ -17097,13 +20464,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -17117,23 +20484,61 @@ "schema": { "type": "object", "properties": { - "email": { + "UseStandardizedSchema": { "type": "string" }, - "webhook": { + "webhookAuthHeaderName": { + "type": "string" + }, + "webhookAuthHeaders": { + "type": "string" + }, + "webhookAuthHeaderValue": { + "type": "string" + }, + "webhookAuthPassword": { + "type": "string" + }, + "webhookAuthToken": { + "type": "string" + }, + "webhookAuthType": { "type": "string" }, + "webhookAuthUsername": { + "type": "string" + }, + "email": { + "type": "string", + "description": "(auto) from CippNotificationForm.jsx" + }, + "webhook": { + "type": "string", + "description": "(auto) from CippNotificationForm.jsx" + }, "logsToInclude": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "(auto) from CippNotificationForm.jsx" }, "Severity": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "(auto) from CippNotificationForm.jsx" }, "onePerTenant": { - "type": "boolean" + "type": "boolean", + "description": "(auto) from CippNotificationForm.jsx" }, "sendtoIntegration": { - "type": "boolean" + "type": "boolean", + "description": "(auto) from CippNotificationForm.jsx" } } } @@ -17165,13 +20570,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -17242,13 +20647,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -17266,91 +20671,139 @@ "type": "object", "properties": { "tenantFilter": { - "type": "string" + "type": "string", + "description": "Target tenant domain" }, "user": { "type": "array", "items": { "type": "string" - } + }, + "description": "Array of user principal names to offboard" }, "ConvertToShared": { - "type": "boolean" + "type": "boolean", + "description": "Convert user mailbox to shared mailbox" }, "HideFromGAL": { - "type": "boolean" + "type": "boolean", + "description": "Hide mailbox from Global Address List" }, "removeCalendarInvites": { - "type": "boolean" + "type": "boolean", + "description": "Remove calendar invites for the user" }, "removePermissions": { - "type": "boolean" + "type": "boolean", + "description": "Remove mailbox permissions granted to the user" }, "removeCalendarPermissions": { - "type": "boolean" + "type": "boolean", + "description": "Remove calendar permissions granted to the user" }, "RemoveRules": { - "type": "boolean" + "type": "boolean", + "description": "Remove mailbox rules" }, "RemoveMobile": { - "type": "boolean" + "type": "boolean", + "description": "Remove mobile device associations" }, "RemoveGroups": { - "type": "boolean" + "type": "boolean", + "description": "Remove user from all groups" }, "RemoveLicenses": { - "type": "boolean" + "type": "boolean", + "description": "Remove all licenses from the user" }, "RevokeSessions": { - "type": "boolean" + "type": "boolean", + "description": "Revoke all active sessions" }, "DisableSignIn": { - "type": "boolean" + "type": "boolean", + "description": "Disable user sign-in" }, "ClearImmutableId": { - "type": "boolean" + "type": "boolean", + "description": "Clear the ImmutableId (on-premises sync attribute)" }, "ResetPass": { - "type": "boolean" + "type": "boolean", + "description": "Reset user password" }, "RemoveMFADevices": { - "type": "boolean" + "type": "boolean", + "description": "Remove all MFA devices" }, "RemoveTeamsPhoneDID": { - "type": "boolean" + "type": "boolean", + "description": "Remove Teams phone number assignment" }, "DeleteUser": { - "type": "boolean" + "type": "boolean", + "description": "Permanently delete the user account" }, "AccessNoAutomap": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "Grant mailbox access without automapping (user selection)" }, "AccessAutomap": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "Grant mailbox access with automapping (user selection)" }, "OnedriveAccess": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "Grant OneDrive access (user selection)" }, "forward": { - "type": "string" + "type": "string", + "description": "Email address to forward mail to" }, "disableForwarding": { - "type": "boolean" + "type": "boolean", + "description": "Disable mail forwarding" }, "KeepCopy": { - "type": "boolean" + "type": "boolean", + "description": "Keep copy of forwarded messages in mailbox" }, "OOO": { - "type": "string" + "type": "string", + "description": "Out of office message" }, "Scheduled": { - "$ref": "#/components/schemas/ScheduledTask" + "allOf": [ + { + "$ref": "#/components/schemas/ScheduledTask" + } + ], + "description": "Schedule offboarding for a future time" }, "PostExecution": { - "$ref": "#/components/schemas/PostExecution" + "allOf": [ + { + "$ref": "#/components/schemas/PostExecution" + } + ], + "description": "Post-execution notifications" }, "reference": { - "type": "string" + "type": "string", + "description": "Reference identifier for tracking" } }, "required": [ @@ -17385,13 +20838,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -17416,7 +20869,7 @@ "type": "object", "properties": { "OffloadFunctions": { - "type": "boolean" + "type": "string" } } } @@ -17448,13 +20901,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -17469,31 +20922,48 @@ "type": "object", "properties": { "addMissingGroups": { - "type": "string" + "type": "string", + "description": "addMissingGroups parameter" }, "autoMapRoles": { - "type": "string" + "type": "string", + "description": "autoMapRoles parameter" }, "Cancel": { - "type": "string" + "type": "string", + "description": "Cancel parameter" }, "gdapRoles": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "gdapRoles parameter" }, "id": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "id parameter" }, "ignoreMissingRoles": { - "type": "boolean" + "type": "boolean", + "description": "ignoreMissingRoles parameter" }, "remapRoles": { - "type": "string" + "type": "string", + "description": "remapRoles parameter" }, "Retry": { - "type": "string" + "type": "string", + "description": "Retry parameter" }, "standardsExcludeAllTenants": { - "type": "boolean" + "type": "boolean", + "description": "standardsExcludeAllTenants parameter" } } } @@ -17525,13 +20995,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -17590,13 +21060,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -17662,13 +21132,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -17725,13 +21195,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -17802,13 +21272,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -17898,13 +21368,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -17963,13 +21433,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -18028,19 +21498,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "description": "Merges new permissions from the SAM manifest into the AppPermissions entry for CIPP-SAM.\n .FUNCTIONALITY\n Entrypoint\n .ROLE\n CIPP.AppSettings.ReadWrite\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", + "description": "Merges new permissions from the SAM manifest into the AppPermissions entry for CIPP-SAM.", "x-cipp-role": "CIPP.AppSettings.ReadWrite" } }, @@ -18067,13 +21537,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -18093,109 +21563,24 @@ "Identity": { "type": "string" }, - "tenantFilter": { + "PolicyName": { "type": "string" }, - "Type": { - "type": "string" - } - }, - "required": [ - "tenantFilter" - ] - } - } - } - } - } - }, - "/api/ExecRemoveMailboxRule": { - "post": { - "summary": "ExecRemoveMailboxRule", - "tags": [ - "Email-Exchange > Administration" - ], - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StandardResults" - } - } - } - }, - "400": { - "description": "Bad request — missing required field or invalid input" - }, - "401": { - "description": "Unauthorized — invalid or missing bearer token" - }, - "403": { - "description": "Forbidden — caller lacks the required RBAC role" - }, - "500": { - "description": "Internal server error" - } - }, - "x-cipp-role": "Exchange.Mailbox.ReadWrite", - "parameters": [ - { - "name": "ruleId", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "ruleName", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "$ref": "#/components/parameters/tenantFilter" - }, - { - "name": "userPrincipalName", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "ruleId": { + "RecipientAddress": { "type": "string" }, - "ruleName": { + "SenderAddress": { "type": "string" }, - "TenantFilter": { + "tenantFilter": { "type": "string" }, - "userPrincipalName": { + "Type": { "type": "string" } }, "required": [ - "TenantFilter" + "tenantFilter" ] } } @@ -18203,11 +21588,11 @@ } } }, - "/api/ExecRemoveRestrictedUser": { + "/api/ExecRemoveAdminRole": { "post": { - "summary": "ExecRemoveRestrictedUser", + "summary": "Removes a user from an assigned Microsoft Entra admin role.", "tags": [ - "Email-Exchange > Administration" + "Identity > Administration > Users" ], "security": [ { @@ -18226,20 +21611,20 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "description": "Removes a user from the restricted senders list in Exchange Online.\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", - "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "description": "Removes a user from an assigned Microsoft Entra admin role.", + "x-cipp-role": "Identity.Role.ReadWrite", "requestBody": { "required": true, "content": { @@ -18247,11 +21632,17 @@ "schema": { "type": "object", "properties": { - "SenderAddress": { + "RoleId": { + "type": "string" + }, + "RoleName": { "type": "string" }, "tenantFilter": { "type": "string" + }, + "Users": { + "type": "string" } }, "required": [ @@ -18263,11 +21654,11 @@ } } }, - "/api/ExecRemoveTeamsVoicePhoneNumberAssignment": { + "/api/ExecRemoveEnrollmentProfile": { "post": { - "summary": "ExecRemoveTeamsVoicePhoneNumberAssignment", + "summary": "Deletes an Apple ADE or Android Enterprise enrollment profile.", "tags": [ - "Teams-Sharepoint" + "Endpoint > MEM" ], "security": [ { @@ -18286,19 +21677,20 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Teams.Voice.ReadWrite", + "description": "Deletes an Apple ADE or Android Enterprise enrollment profile.", + "x-cipp-role": "Endpoint.MEM.ReadWrite", "requestBody": { "required": true, "content": { @@ -18306,17 +21698,23 @@ "schema": { "type": "object", "properties": { - "AssignedTo": { + "displayName": { "type": "string" }, - "PhoneNumber": { + "id": { "type": "string" }, - "PhoneNumberType": { + "profileId": { + "type": "string" + }, + "profileType": { "type": "string" }, "tenantFilter": { "type": "string" + }, + "tokenId": { + "type": "string" } }, "required": [ @@ -18328,11 +21726,11 @@ } } }, - "/api/ExecRemoveTenant": { + "/api/ExecRemoveMailboxRule": { "post": { - "summary": "ExecRemoveTenant", + "summary": "ExecRemoveMailboxRule", "tags": [ - "CIPP > Settings" + "Email-Exchange > Administration" ], "security": [ { @@ -18351,19 +21749,48 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Tenant.Administration.ReadWrite", + "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "parameters": [ + { + "name": "ruleId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "ruleName", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "userPrincipalName", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], "requestBody": { "required": true, "content": { @@ -18371,21 +21798,33 @@ "schema": { "type": "object", "properties": { - "TenantID": { + "ruleId": { + "type": "string" + }, + "ruleName": { + "type": "string" + }, + "TenantFilter": { + "type": "string" + }, + "userPrincipalName": { "type": "string" } - } + }, + "required": [ + "TenantFilter" + ] } } } } } }, - "/api/ExecRenameAPDevice": { + "/api/ExecRemoveRestrictedUser": { "post": { - "summary": "ExecRenameAPDevice", + "summary": "Removes a user from the restricted senders list in Exchange Online.", "tags": [ - "Endpoint > Autopilot" + "Email-Exchange > Administration" ], "security": [ { @@ -18404,19 +21843,20 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Endpoint.Autopilot.ReadWrite", + "description": "Removes a user from the restricted senders list in Exchange Online.", + "x-cipp-role": "Exchange.Mailbox.ReadWrite", "requestBody": { "required": true, "content": { @@ -18424,13 +21864,7 @@ "schema": { "type": "object", "properties": { - "deviceId": { - "type": "string" - }, - "displayName": { - "type": "string" - }, - "serialNumber": { + "SenderAddress": { "type": "string" }, "tenantFilter": { @@ -18446,11 +21880,11 @@ } } }, - "/api/ExecReprocessUserLicenses": { + "/api/ExecRemoveSnooze": { "post": { - "summary": "ExecReprocessUserLicenses", + "summary": "ExecRemoveSnooze", "tags": [ - "Identity > Administration > Users" + "CIPP > Core" ], "security": [ { @@ -18469,22 +21903,22 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Identity.User.ReadWrite", + "x-cipp-role": "CIPP.Alert.ReadWrite", "parameters": [ { - "name": "ID", + "name": "PartitionKey", "in": "query", "required": false, "schema": { @@ -18492,10 +21926,7 @@ } }, { - "$ref": "#/components/parameters/tenantFilter" - }, - { - "name": "userPrincipalName", + "name": "RowKey", "in": "query", "required": false, "schema": { @@ -18510,30 +21941,24 @@ "schema": { "type": "object", "properties": { - "ID": { + "PartitionKey": { "type": "string" }, - "tenantFilter": { - "type": "string" - }, - "userPrincipalName": { + "RowKey": { "type": "string" } - }, - "required": [ - "tenantFilter" - ] + } } } } } } }, - "/api/ExecResetMFA": { + "/api/ExecRemoveTeamsVoicePhoneNumberAssignment": { "post": { - "summary": "ExecResetMFA", + "summary": "ExecRemoveTeamsVoicePhoneNumberAssignment", "tags": [ - "Identity > Administration > Users" + "Teams-Sharepoint" ], "security": [ { @@ -18552,32 +21977,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Identity.User.ReadWrite", - "parameters": [ - { - "name": "ID", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "$ref": "#/components/parameters/tenantFilter" - } - ], + "x-cipp-role": "Teams.Voice.ReadWrite", "requestBody": { "required": true, "content": { @@ -18585,7 +21997,13 @@ "schema": { "type": "object", "properties": { - "ID": { + "AssignedTo": { + "type": "string" + }, + "PhoneNumber": { + "type": "string" + }, + "PhoneNumberType": { "type": "string" }, "tenantFilter": { @@ -18601,11 +22019,11 @@ } } }, - "/api/ExecResetPass": { + "/api/ExecRemoveTenant": { "post": { - "summary": "ExecResetPass", + "summary": "ExecRemoveTenant", "tags": [ - "Identity > Administration > Users" + "CIPP > Settings" ], "security": [ { @@ -18624,48 +22042,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Identity.User.ReadWrite", - "parameters": [ - { - "name": "displayName", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "ID", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "MustChange", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "$ref": "#/components/parameters/tenantFilter" - } - ], + "x-cipp-role": "Tenant.Administration.ReadWrite", "requestBody": { "required": true, "content": { @@ -18673,33 +22062,21 @@ "schema": { "type": "object", "properties": { - "displayName": { - "type": "string" - }, - "ID": { - "type": "string" - }, - "MustChange": { - "type": "string" - }, - "tenantFilter": { + "TenantID": { "type": "string" } - }, - "required": [ - "tenantFilter" - ] + } } } } } } }, - "/api/ExecRestoreBackup": { + "/api/ExecRenameAPDevice": { "post": { - "summary": "ExecRestoreBackup", + "summary": "ExecRenameAPDevice", "tags": [ - "CIPP > Settings" + "Endpoint > Autopilot" ], "security": [ { @@ -18718,20 +22095,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "CIPP.AppSettings.ReadWrite", - "x-cipp-dynamic-options": true, + "x-cipp-role": "Endpoint.Autopilot.ReadWrite", "requestBody": { "required": true, "content": { @@ -18739,24 +22115,33 @@ "schema": { "type": "object", "properties": { - "BackupName": { + "deviceId": { "type": "string" }, - "SelectedTypes": { + "displayName": { + "type": "string" + }, + "serialNumber": { + "type": "string" + }, + "tenantFilter": { "type": "string" } - } + }, + "required": [ + "tenantFilter" + ] } } } } } }, - "/api/ExecRestoreDeleted": { - "post": { - "summary": "ExecRestoreDeleted", + "/api/ExecReportBuilderTemplate": { + "delete": { + "summary": "ExecReportBuilderTemplate", "tags": [ - "Identity > Administration > Users" + "Tools" ], "security": [ { @@ -18775,32 +22160,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Tenant.Directory.ReadWrite", - "parameters": [ - { - "name": "ID", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "$ref": "#/components/parameters/tenantFilter" - } - ], + "x-cipp-role": "CIPP.Core.ReadWrite", "requestBody": { "required": true, "content": { @@ -18808,31 +22180,33 @@ "schema": { "type": "object", "properties": { - "displayName": { + "Action": { "type": "string" }, - "ID": { + "Blocks": { "type": "string" }, - "tenantFilter": { + "GUID": { "type": "string" }, - "userPrincipalName": { - "type": "string" + "Name": { + "type": "object", + "properties": { + "Length": { + "type": "string" + } + } } - }, - "required": [ - "tenantFilter" - ] + } } } } } } }, - "/api/ExecRevokeSessions": { + "/api/ExecReprocessUserLicenses": { "post": { - "summary": "ExecRevokeSessions", + "summary": "ExecReprocessUserLicenses", "tags": [ "Identity > Administration > Users" ], @@ -18853,13 +22227,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -18868,7 +22242,7 @@ "x-cipp-role": "Identity.User.ReadWrite", "parameters": [ { - "name": "id", + "name": "ID", "in": "query", "required": false, "schema": { @@ -18879,7 +22253,7 @@ "$ref": "#/components/parameters/tenantFilter" }, { - "name": "Username", + "name": "userPrincipalName", "in": "query", "required": false, "schema": { @@ -18894,13 +22268,13 @@ "schema": { "type": "object", "properties": { - "id": { + "ID": { "type": "string" }, "tenantFilter": { "type": "string" }, - "Username": { + "userPrincipalName": { "type": "string" } }, @@ -18913,11 +22287,11 @@ } } }, - "/api/ExecRunBackup": { - "get": { - "summary": "ExecRunBackup", + "/api/ExecResetMFA": { + "post": { + "summary": "ExecResetMFA", "tags": [ - "CIPP > Settings" + "Identity > Administration > Users" ], "security": [ { @@ -18936,26 +22310,60 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "CIPP.AppSettings.ReadWrite" + "x-cipp-role": "Identity.User.ReadWrite", + "parameters": [ + { + "name": "ID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ID": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } } }, - "/api/ExecRunTenantGroupRule": { + "/api/ExecResetPass": { "post": { - "summary": "Execute tenant group dynamic rules immediately", + "summary": "ExecResetPass", "tags": [ - "CIPP > Settings" + "Identity > Administration > Users" ], "security": [ { @@ -18974,28 +22382,46 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "description": "This function executes dynamic tenant group rules for immediate membership updates\n .FUNCTIONALITY\n Entrypoint,AnyTenant\n .ROLE\n Tenant.Groups.ReadWrite\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", - "x-cipp-role": "Tenant.Groups.ReadWrite", + "x-cipp-role": "Identity.User.ReadWrite", "parameters": [ { - "name": "groupId", + "name": "displayName", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "ID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "MustChange", "in": "query", "required": false, "schema": { "type": "string" } + }, + { + "$ref": "#/components/parameters/tenantFilter" } ], "requestBody": { @@ -19005,19 +22431,31 @@ "schema": { "type": "object", "properties": { - "groupId": { + "displayName": { + "type": "string" + }, + "ID": { + "type": "string" + }, + "MustChange": { + "type": "string" + }, + "tenantFilter": { "type": "string" } - } + }, + "required": [ + "tenantFilter" + ] } } } } } }, - "/api/ExecSAMAppPermissions": { + "/api/ExecRestoreBackup": { "post": { - "summary": "ExecSAMAppPermissions", + "summary": "ExecRestoreBackup", "tags": [ "CIPP > Settings" ], @@ -19038,29 +22476,20 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "CIPP.SuperAdmin.ReadWrite", - "parameters": [ - { - "name": "Action", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - } - ], + "x-cipp-role": "CIPP.AppSettings.ReadWrite", + "x-cipp-dynamic-options": true, "requestBody": { "required": true, "content": { @@ -19068,7 +22497,10 @@ "schema": { "type": "object", "properties": { - "Permissions": { + "BackupName": { + "type": "string" + }, + "SelectedTypes": { "type": "string" } } @@ -19078,11 +22510,11 @@ } } }, - "/api/ExecSAMRoles": { + "/api/ExecRestoreDeleted": { "post": { - "summary": "ExecSAMRoles", + "summary": "ExecRestoreDeleted", "tags": [ - "CIPP > Settings" + "Identity > Administration > Users" ], "security": [ { @@ -19101,27 +22533,30 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "CIPP.SuperAdmin.ReadWrite", + "x-cipp-role": "Tenant.Directory.ReadWrite", "parameters": [ { - "name": "Action", + "name": "ID", "in": "query", "required": false, "schema": { "type": "string" } + }, + { + "$ref": "#/components/parameters/tenantFilter" } ], "requestBody": { @@ -19131,24 +22566,33 @@ "schema": { "type": "object", "properties": { - "Roles": { + "displayName": { "type": "string" }, - "Tenants": { + "ID": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "userPrincipalName": { "type": "string" } - } + }, + "required": [ + "tenantFilter" + ] } } } } } }, - "/api/ExecSAMSetup": { + "/api/ExecRevokeSessions": { "post": { - "summary": "ExecSAMSetup", + "summary": "ExecRevokeSessions", "tags": [ - "CIPP > Setup" + "Identity > Administration > Users" ], "security": [ { @@ -19167,54 +22611,22 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "CIPP.AppSettings.ReadWrite", + "x-cipp-role": "Identity.User.ReadWrite", "parameters": [ { - "name": "CheckSetupProcess", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "code", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "count", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "CreateSAM", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "error", + "name": "id", "in": "query", "required": false, "schema": { @@ -19222,15 +22634,10 @@ } }, { - "name": "error_description", - "in": "query", - "required": false, - "schema": { - "type": "string" - } + "$ref": "#/components/parameters/tenantFilter" }, { - "name": "step", + "name": "Username", "in": "query", "required": false, "schema": { @@ -19245,33 +22652,68 @@ "schema": { "type": "object", "properties": { - "applicationid": { - "type": "string" - }, - "applicationsecret": { - "type": "string" - }, - "RefreshToken": { + "id": { "type": "string" }, - "setkeys": { + "tenantFilter": { "type": "string" }, - "tenantid": { + "Username": { "type": "string" } - } + }, + "required": [ + "tenantFilter" + ] } } } } } }, - "/api/ExecScheduleMailboxVacation": { + "/api/ExecRunBackup": { + "get": { + "summary": "ExecRunBackup", + "tags": [ + "CIPP > Settings" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.AppSettings.ReadWrite" + } + }, + "/api/ExecRunTenantGroupRule": { "post": { - "summary": "ExecScheduleMailboxVacation", + "summary": "Execute tenant group dynamic rules immediately", "tags": [ - "Email-Exchange > Administration" + "CIPP > Settings" ], "security": [ { @@ -19290,19 +22732,30 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "description": "This function executes dynamic tenant group rules for immediate membership updates", + "x-cipp-role": "Tenant.Groups.ReadWrite", + "parameters": [ + { + "name": "groupId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], "requestBody": { "required": true, "content": { @@ -19310,57 +22763,21 @@ "schema": { "type": "object", "properties": { - "autoMap": { - "type": "string" - }, - "calendarPermission": { - "type": "string" - }, - "canViewPrivateItems": { - "type": "string" - }, - "delegates": { - "type": "string" - }, - "endDate": { - "type": "string" - }, - "includeCalendar": { - "type": "string" - }, - "mailboxOwners": { - "type": "string" - }, - "permissionTypes": { - "type": "string" - }, - "postExecution": { - "$ref": "#/components/schemas/PostExecution" - }, - "reference": { - "type": "string" - }, - "startDate": { - "type": "string" - }, - "tenantFilter": { + "groupId": { "type": "string" } - }, - "required": [ - "tenantFilter" - ] + } } } } } } }, - "/api/ExecScheduleOOOVacation": { + "/api/ExecSAMAppPermissions": { "post": { - "summary": "ExecScheduleOOOVacation", + "summary": "ExecSAMAppPermissions", "tags": [ - "Email-Exchange > Administration" + "CIPP > Settings" ], "security": [ { @@ -19379,19 +22796,29 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "x-cipp-role": "CIPP.SuperAdmin.ReadWrite", + "parameters": [ + { + "name": "Action", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], "requestBody": { "required": true, "content": { @@ -19399,45 +22826,21 @@ "schema": { "type": "object", "properties": { - "endDate": { - "type": "string" - }, - "externalMessage": { - "type": "string" - }, - "internalMessage": { - "type": "string" - }, - "postExecution": { - "$ref": "#/components/schemas/PostExecution" - }, - "reference": { - "type": "string" - }, - "startDate": { - "type": "string" - }, - "tenantFilter": { - "type": "string" - }, - "Users": { + "Permissions": { "type": "string" } - }, - "required": [ - "tenantFilter" - ] + } } } } } } }, - "/api/ExecSchedulerBillingRun": { - "get": { - "summary": "ExecSchedulerBillingRun", + "/api/ExecSAMRoles": { + "post": { + "summary": "ExecSAMRoles", "tags": [ - "Uncategorized" + "CIPP > Settings" ], "security": [ { @@ -19456,26 +22859,54 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Scheduler.Billing.ReadWrite" + "x-cipp-role": "CIPP.SuperAdmin.ReadWrite", + "parameters": [ + { + "name": "Action", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Roles": { + "type": "string" + }, + "Tenants": { + "type": "string" + } + } + } + } + } + } } }, - "/api/ExecSendOrgMessage": { - "get": { - "summary": "ExecSendOrgMessage", + "/api/ExecSAMSetup": { + "post": { + "summary": "ExecSAMSetup", "tags": [ - "Uncategorized" + "CIPP > Setup" ], "security": [ { @@ -19494,22 +22925,22 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Tenant.Directory.ReadWrite", + "x-cipp-role": "CIPP.AppSettings.ReadWrite", "parameters": [ { - "name": "freq", + "name": "CheckSetupProcess", "in": "query", "required": false, "schema": { @@ -19517,7 +22948,7 @@ } }, { - "name": "ID", + "name": "code", "in": "query", "required": false, "schema": { @@ -19525,10 +22956,15 @@ } }, { - "$ref": "#/components/parameters/tenantFilter" + "name": "count", + "in": "query", + "required": false, + "schema": { + "type": "string" + } }, { - "name": "type", + "name": "CreateSAM", "in": "query", "required": false, "schema": { @@ -19536,21 +22972,64 @@ } }, { - "name": "URL", + "name": "error", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "error_description", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "step", "in": "query", "required": false, "schema": { "type": "string" } } - ] + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "applicationid": { + "type": "string" + }, + "applicationsecret": { + "type": "string" + }, + "RefreshToken": { + "type": "string" + }, + "setkeys": { + "type": "string" + }, + "tenantid": { + "type": "string" + } + } + } + } + } + } } }, - "/api/ExecSendPush": { + "/api/ExecSPOVersionCleanup": { "post": { - "summary": "ExecSendPush", + "summary": "ExecSPOVersionCleanup", "tags": [ - "Identity > Administration > Users" + "Teams-Sharepoint" ], "security": [ { @@ -19569,19 +23048,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Identity.User.Read", + "x-cipp-role": "Sharepoint.Site.ReadWrite", "requestBody": { "required": true, "content": { @@ -19589,15 +23068,27 @@ "schema": { "type": "object", "properties": { - "TenantFilter": { + "BatchDeleteMode": { "type": "string" }, - "UserEmail": { + "DeleteOlderThanDays": { + "type": "string" + }, + "MajorVersionLimit": { + "type": "string" + }, + "MajorWithMinorVersionsLimit": { + "type": "string" + }, + "SiteUrl": { + "type": "string" + }, + "tenantFilter": { "type": "string" } }, "required": [ - "TenantFilter" + "tenantFilter" ] } } @@ -19605,11 +23096,11 @@ } } }, - "/api/ExecServicePrincipals": { - "get": { - "summary": "ExecServicePrincipals", + "/api/ExecSSOSetup": { + "post": { + "summary": "ExecSSOSetup", "tags": [ - "CIPP > Core" + "CIPP > Setup" ], "security": [ { @@ -19628,19 +23119,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Tenant.Application.ReadWrite", + "x-cipp-role": "CIPP.AppSettings.ReadWrite", "parameters": [ { "name": "Action", @@ -19649,39 +23140,36 @@ "schema": { "type": "string" } - }, - { - "name": "AppId", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "Id", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "Select", - "in": "query", - "required": false, - "schema": { - "type": "string" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Action": { + "type": "string" + }, + "multiTenant": { + "type": "boolean" + }, + "targetUrl": { + "type": "string" + } + } + } } } - ] + } } }, - "/api/ExecSetAPDeviceGroupTag": { + "/api/ExecScheduleForwardingVacation": { "post": { - "summary": "ExecSetAPDeviceGroupTag", + "summary": "ExecScheduleForwardingVacation", "tags": [ - "Endpoint > Autopilot" + "Email-Exchange > Administration" ], "security": [ { @@ -19700,19 +23188,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Endpoint.Autopilot.ReadWrite", + "x-cipp-role": "Exchange.Mailbox.ReadWrite", "requestBody": { "required": true, "content": { @@ -19720,17 +23208,35 @@ "schema": { "type": "object", "properties": { - "deviceId": { + "endDate": { "type": "string" }, - "groupTag": { + "ForwardExternal": { "type": "string" }, - "serialNumber": { + "ForwardInternal": { + "type": "string" + }, + "forwardOption": { + "type": "string" + }, + "KeepCopy": { + "type": "string" + }, + "postExecution": { + "$ref": "#/components/schemas/PostExecution" + }, + "reference": { + "type": "string" + }, + "startDate": { "type": "string" }, "tenantFilter": { "type": "string" + }, + "Users": { + "type": "string" } }, "required": [ @@ -19742,11 +23248,11 @@ } } }, - "/api/ExecSetCIPPAutoBackup": { + "/api/ExecScheduleMailboxVacation": { "post": { - "summary": "ExecSetCIPPAutoBackup", + "summary": "ExecScheduleMailboxVacation", "tags": [ - "CIPP > Core" + "Email-Exchange > Administration" ], "security": [ { @@ -19765,19 +23271,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "CIPP.Backup.ReadWrite", + "x-cipp-role": "Exchange.Mailbox.ReadWrite", "requestBody": { "required": true, "content": { @@ -19785,19 +23291,55 @@ "schema": { "type": "object", "properties": { - "Enabled": { - "type": "boolean" + "autoMap": { + "type": "string" + }, + "calendarPermission": { + "type": "string" + }, + "canViewPrivateItems": { + "type": "string" + }, + "delegates": { + "type": "string" + }, + "endDate": { + "type": "string" + }, + "includeCalendar": { + "type": "string" + }, + "mailboxOwners": { + "type": "string" + }, + "permissionTypes": { + "type": "string" + }, + "postExecution": { + "$ref": "#/components/schemas/PostExecution" + }, + "reference": { + "type": "string" + }, + "startDate": { + "type": "string" + }, + "tenantFilter": { + "type": "string" } - } + }, + "required": [ + "tenantFilter" + ] } } } } } }, - "/api/ExecSetCalendarProcessing": { + "/api/ExecScheduleOOOVacation": { "post": { - "summary": "ExecSetCalendarProcessing", + "summary": "ExecScheduleOOOVacation", "tags": [ "Email-Exchange > Administration" ], @@ -19818,13 +23360,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -19838,61 +23380,43 @@ "schema": { "type": "object", "properties": { - "additionalResponse": { - "type": "string" - }, - "addOrganizerToSubject": { - "type": "string" - }, - "allowConflicts": { - "type": "string" - }, - "allowRecurringMeetings": { - "type": "string" - }, - "automaticallyAccept": { - "type": "string" - }, - "automaticallyProcess": { + "AutoDeclineFutureRequestsWhenOOF": { "type": "string" }, - "bookingWindowInDays": { - "type": "string" - }, - "deleteComments": { + "CreateOOFEvent": { "type": "string" }, - "deleteSubject": { + "DeclineEventsForScheduledOOF": { "type": "string" }, - "maxConflicts": { + "DeclineMeetingMessage": { "type": "string" }, - "maximumDurationInMinutes": { + "endDate": { "type": "string" }, - "minimumDurationInMinutes": { + "externalMessage": { "type": "string" }, - "processExternalMeetingMessages": { + "internalMessage": { "type": "string" }, - "removeCanceledMeetings": { + "OOFEventSubject": { "type": "string" }, - "removeOldMeetingMessages": { - "type": "string" + "postExecution": { + "$ref": "#/components/schemas/PostExecution" }, - "removePrivateProperty": { + "reference": { "type": "string" }, - "scheduleOnlyDuringWorkHours": { + "startDate": { "type": "string" }, "tenantFilter": { "type": "string" }, - "UPN": { + "Users": { "type": "string" } }, @@ -19905,11 +23429,124 @@ } } }, - "/api/ExecSetCloudManaged": { + "/api/ExecSchedulerBillingRun": { + "get": { + "summary": "ExecSchedulerBillingRun", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Scheduler.Billing.ReadWrite" + } + }, + "/api/ExecSendOrgMessage": { + "get": { + "summary": "ExecSendOrgMessage", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Directory.ReadWrite", + "parameters": [ + { + "name": "freq", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "ID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "type", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "URL", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/ExecSendPush": { "post": { - "summary": "ExecSetCloudManaged", + "summary": "ExecSendPush", "tags": [ - "Identity" + "Identity > Administration > Users" ], "security": [ { @@ -19928,20 +23565,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "description": "Sets the cloud-managed status of a user, group, or contact.\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", - "x-cipp-role": "Identity.DirSync.ReadWrite", + "x-cipp-role": "Identity.User.Read", "requestBody": { "required": true, "content": { @@ -19949,24 +23585,15 @@ "schema": { "type": "object", "properties": { - "displayName": { - "type": "string" - }, - "ID": { - "type": "string" - }, - "isCloudManaged": { - "type": "string" - }, - "tenantFilter": { + "TenantFilter": { "type": "string" }, - "type": { + "UserEmail": { "type": "string" } }, "required": [ - "tenantFilter" + "TenantFilter" ] } } @@ -19974,11 +23601,83 @@ } } }, - "/api/ExecSetLitigationHold": { + "/api/ExecServicePrincipals": { + "get": { + "summary": "ExecServicePrincipals", + "tags": [ + "CIPP > Core" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Application.ReadWrite", + "parameters": [ + { + "name": "Action", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "AppId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Id", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Select", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/ExecSetAPDeviceGroupTag": { "post": { - "summary": "ExecSetLitigationHold", + "summary": "ExecSetAPDeviceGroupTag", "tags": [ - "Email-Exchange > Administration" + "Endpoint > Autopilot" ], "security": [ { @@ -19997,19 +23696,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "x-cipp-role": "Endpoint.Autopilot.ReadWrite", "requestBody": { "required": true, "content": { @@ -20017,20 +23716,17 @@ "schema": { "type": "object", "properties": { - "days": { + "deviceId": { "type": "string" }, - "disable": { + "groupTag": { "type": "string" }, - "Identity": { + "serialNumber": { "type": "string" }, "tenantFilter": { "type": "string" - }, - "UPN": { - "type": "string" } }, "required": [ @@ -20042,9 +23738,9 @@ } } }, - "/api/ExecSetMailboxEmailSize": { + "/api/ExecSetCASMailbox": { "post": { - "summary": "ExecSetMailboxEmailSize", + "summary": "ExecSetCASMailbox", "tags": [ "Email-Exchange > Administration" ], @@ -20065,13 +23761,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -20085,20 +23781,14 @@ "schema": { "type": "object", "properties": { - "id": { - "type": "string" - }, - "maxReceiveSize": { + "DisplayName": { "type": "string" }, - "maxSendSize": { + "Identity": { "type": "string" }, "tenantFilter": { "type": "string" - }, - "UPN": { - "type": "string" } }, "required": [ @@ -20110,11 +23800,11 @@ } } }, - "/api/ExecSetMailboxLocale": { + "/api/ExecSetCIPPAutoBackup": { "post": { - "summary": "ExecSetMailboxLocale", + "summary": "ExecSetCIPPAutoBackup", "tags": [ - "Email-Exchange > Administration" + "CIPP > Core" ], "security": [ { @@ -20133,19 +23823,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "x-cipp-role": "CIPP.Backup.ReadWrite", "requestBody": { "required": true, "content": { @@ -20153,28 +23843,19 @@ "schema": { "type": "object", "properties": { - "locale": { - "type": "string" - }, - "tenantFilter": { - "type": "string" - }, - "user": { - "type": "string" + "Enabled": { + "type": "boolean" } - }, - "required": [ - "tenantFilter" - ] + } } } } } } }, - "/api/ExecSetMailboxQuota": { + "/api/ExecSetCalendarProcessing": { "post": { - "summary": "ExecSetMailboxQuota", + "summary": "ExecSetCalendarProcessing", "tags": [ "Email-Exchange > Administration" ], @@ -20195,13 +23876,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -20215,27 +23896,66 @@ "schema": { "type": "object", "properties": { - "IssueWarningQuota": { + "additionalResponse": { "type": "string" }, - "ProhibitSendQuota": { + "addOrganizerToSubject": { "type": "string" }, - "ProhibitSendReceiveQuota": { + "allowConflicts": { "type": "string" }, - "quota": { + "allowRecurringMeetings": { "type": "string" }, - "tenantfilter": { + "automaticallyAccept": { "type": "string" }, - "user": { + "automaticallyProcess": { + "type": "string" + }, + "bookingWindowInDays": { + "type": "string" + }, + "deleteComments": { + "type": "string" + }, + "deleteSubject": { + "type": "string" + }, + "maxConflicts": { + "type": "string" + }, + "maximumDurationInMinutes": { + "type": "string" + }, + "minimumDurationInMinutes": { + "type": "string" + }, + "processExternalMeetingMessages": { + "type": "string" + }, + "removeCanceledMeetings": { + "type": "string" + }, + "removeOldMeetingMessages": { + "type": "string" + }, + "removePrivateProperty": { + "type": "string" + }, + "scheduleOnlyDuringWorkHours": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "UPN": { "type": "string" } }, "required": [ - "tenantfilter" + "tenantFilter" ] } } @@ -20243,11 +23963,11 @@ } } }, - "/api/ExecSetMailboxRetentionPolicies": { + "/api/ExecSetCloudManaged": { "post": { - "summary": "ExecSetMailboxRetentionPolicies", + "summary": "Sets the cloud-managed status of a user, group, or contact.", "tags": [ - "Email-Exchange > Administration > Mailbox Retention" + "Identity" ], "security": [ { @@ -20266,24 +23986,20 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Exchange.Mailbox.ReadWrite", - "parameters": [ - { - "$ref": "#/components/parameters/tenantFilter" - } - ], + "description": "Sets the cloud-managed status of a user, group, or contact.", + "x-cipp-role": "Identity.DirSync.ReadWrite", "requestBody": { "required": true, "content": { @@ -20291,14 +24007,20 @@ "schema": { "type": "object", "properties": { - "Mailboxes": { + "displayName": { "type": "string" }, - "PolicyName": { + "ID": { + "type": "string" + }, + "isCloudManaged": { "type": "string" }, "tenantFilter": { "type": "string" + }, + "type": { + "type": "string" } }, "required": [ @@ -20310,9 +24032,9 @@ } } }, - "/api/ExecSetMailboxRule": { + "/api/ExecSetLitigationHold": { "post": { - "summary": "ExecSetMailboxRule", + "summary": "ExecSetLitigationHold", "tags": [ "Email-Exchange > Administration" ], @@ -20333,13 +24055,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -20353,27 +24075,24 @@ "schema": { "type": "object", "properties": { - "Disable": { - "type": "string" - }, - "Enable": { + "days": { "type": "string" }, - "ruleId": { + "disable": { "type": "string" }, - "ruleName": { + "Identity": { "type": "string" }, - "TenantFilter": { + "tenantFilter": { "type": "string" }, - "userPrincipalName": { + "UPN": { "type": "string" } }, "required": [ - "TenantFilter" + "tenantFilter" ] } } @@ -20381,11 +24100,11 @@ } } }, - "/api/ExecSetMdoAlert": { + "/api/ExecSetMailboxEmailSize": { "post": { - "summary": "ExecSetMdoAlert", + "summary": "ExecSetMailboxEmailSize", "tags": [ - "Security" + "Email-Exchange > Administration" ], "security": [ { @@ -20404,33 +24123,372 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Security.Incident.ReadWrite", - "parameters": [ - { - "name": "Assigned", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "Classification", - "in": "query", - "required": false, - "schema": { + "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "maxReceiveSize": { + "type": "string" + }, + "maxSendSize": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "UPN": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecSetMailboxLocale": { + "post": { + "summary": "ExecSetMailboxLocale", + "tags": [ + "Email-Exchange > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "locale": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "user": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecSetMailboxQuota": { + "post": { + "summary": "ExecSetMailboxQuota", + "tags": [ + "Email-Exchange > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "IssueWarningQuota": { + "type": "string" + }, + "ProhibitSendQuota": { + "type": "string" + }, + "ProhibitSendReceiveQuota": { + "type": "string" + }, + "quota": { + "type": "string" + }, + "tenantfilter": { + "type": "string" + }, + "user": { + "type": "string" + } + }, + "required": [ + "tenantfilter" + ] + } + } + } + } + } + }, + "/api/ExecSetMailboxRetentionPolicies": { + "post": { + "summary": "ExecSetMailboxRetentionPolicies", + "tags": [ + "Email-Exchange > Administration > Mailbox Retention" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Mailboxes": { + "type": "string" + }, + "PolicyName": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecSetMailboxRule": { + "post": { + "summary": "ExecSetMailboxRule", + "tags": [ + "Email-Exchange > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Exchange.Mailbox.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Disable": { + "type": "string" + }, + "Enable": { + "type": "string" + }, + "ruleId": { + "type": "string" + }, + "ruleName": { + "type": "string" + }, + "TenantFilter": { + "type": "string" + }, + "userPrincipalName": { + "type": "string" + } + }, + "required": [ + "TenantFilter" + ] + } + } + } + } + } + }, + "/api/ExecSetMdoAlert": { + "post": { + "summary": "ExecSetMdoAlert", + "tags": [ + "Security" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Security.Incident.ReadWrite", + "parameters": [ + { + "name": "Assigned", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Classification", + "in": "query", + "required": false, + "schema": { "type": "string" } }, @@ -20497,6 +24555,68 @@ } } }, + "/api/ExecSetOneDriveSharing": { + "post": { + "summary": "ExecSetOneDriveSharing", + "tags": [ + "Teams-Sharepoint" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Teams.SharePoint.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "SharingCapability": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "UPN": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, "/api/ExecSetOoO": { "post": { "summary": "ExecSetOoO", @@ -20520,13 +24640,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -20540,9 +24660,21 @@ "schema": { "type": "object", "properties": { + "AutoDeclineFutureRequestsWhenOOF": { + "type": "string" + }, "AutoReplyState": { "type": "string" }, + "CreateOOFEvent": { + "type": "string" + }, + "DeclineEventsForScheduledOOF": { + "type": "string" + }, + "DeclineMeetingMessage": { + "type": "string" + }, "EndTime": { "type": "string" }, @@ -20555,12 +24687,18 @@ "InternalMessage": { "type": "string" }, + "OOFEventSubject": { + "type": "string" + }, "StartTime": { "type": "string" }, "tenantFilter": { "type": "string" }, + "timezone": { + "type": "string" + }, "userId": { "type": "string" } @@ -20597,13 +24735,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -20656,13 +24794,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -20721,13 +24859,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -20786,13 +24924,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -20891,13 +25029,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -21018,13 +25156,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -21086,13 +25224,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -21172,13 +25310,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -21220,6 +25358,71 @@ } } }, + "/api/ExecSnoozeAlert": { + "post": { + "summary": "ExecSnoozeAlert", + "tags": [ + "CIPP > Core" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Alert.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "AlertItem": { + "type": "string" + }, + "CmdletName": { + "type": "string" + }, + "Duration": { + "type": "string" + }, + "TenantFilter": { + "type": "string" + } + }, + "required": [ + "TenantFilter" + ] + } + } + } + } + } + }, "/api/ExecStandardConvert": { "get": { "summary": "ExecStandardConvert", @@ -21243,13 +25446,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -21282,13 +25485,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -21333,13 +25536,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -21408,13 +25611,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -21448,7 +25651,7 @@ }, "/api/ExecSyncDEP": { "post": { - "summary": "ExecSyncDEP", + "summary": "Syncs devices from Apple Business Manager to Intune", "tags": [ "Endpoint > MEM" ], @@ -21469,19 +25672,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "description": "Syncs devices from Apple Business Manager to Intune\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)\n $APIName = $Request.Params.CIPPEndpoint\n $Headers = $Request.Headers", + "description": "Syncs devices from Apple Business Manager to Intune", "x-cipp-role": "Endpoint.MEM.ReadWrite", "requestBody": { "required": true, @@ -21526,13 +25729,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -21582,13 +25785,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -21650,19 +25853,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "description": "This function is used to manage tenant groups in CIPP\n .FUNCTIONALITY\n Entrypoint,AnyTenant\n .ROLE\n Tenant.Groups.ReadWrite\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", + "description": "This function is used to manage tenant groups in CIPP", "x-cipp-role": "Tenant.Groups.ReadWrite", "x-cipp-warnings": [ "customDataformatter is present on the CippFormPage for this endpoint. Form field names and/or types may be renamed or restructured before POST. Verify the transformer function in the frontend page file and correct param names/types accordingly." @@ -21674,14 +25877,20 @@ "schema": { "type": "object", "properties": { - "Action": { + "excludePartnerTenant": { "type": "string" }, + "Action": { + "type": "string", + "description": "Dispatch action. Formatter hard-codes 'AddEdit'. PS1 supports: AddEdit, Delete. Defaults handled by switch." + }, "groupId": { - "type": "string" + "type": "string", + "description": "Group GUID. For edit form this is pre-populated from router query. PS1 auto-generates a new GUID if not provided." }, "groupName": { - "type": "string" + "type": "string", + "description": "Display name for the tenant group." }, "groupDescription": { "type": "string" @@ -21691,25 +25900,30 @@ "enum": [ "static", "dynamic" - ] + ], + "description": "Enum: 'static'|'dynamic'. PS1 defaults to 'static' if not provided." }, "members": { "type": "array", "items": { "type": "string" - } + }, + "description": "Array of {label, value: customerId} tenant objects. Used only for static groups." }, "dynamicRules": { "type": "array", "items": { "type": "string" - } + }, + "description": "Array of rule objects [{property: string, operator: string, value: any}]. Formatter unwraps property/operator from {label,value} objects. Required when groupType='dynamic'." }, "ruleLogic": { - "type": "string" + "type": "string", + "description": "Logic for combining dynamic rules. Enum: 'and'|'or'. PS1 defaults to 'and'." }, "value": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: value" } } } @@ -21718,6 +25932,78 @@ } } }, + "/api/ExecTestRefresh": { + "post": { + "summary": "ExecTestRefresh", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.Tests.ReadWrite", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "testName", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tenantFilter": { + "type": "string" + }, + "testName": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, "/api/ExecTestRun": { "post": { "summary": "ExecTestRun", @@ -21741,13 +26027,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -21755,6 +26041,14 @@ }, "x-cipp-role": "Tenant.Tests.ReadWrite", "parameters": [ + { + "name": "mode", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/tenantFilter" } @@ -21766,6 +26060,9 @@ "schema": { "type": "object", "properties": { + "mode": { + "type": "string" + }, "tenantFilter": { "type": "string" } @@ -21802,13 +26099,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -21822,9 +26119,6 @@ "schema": { "type": "object", "properties": { - "BusinessHoursStart": { - "$ref": "#/components/schemas/LabelValue" - }, "Timezone": { "$ref": "#/components/schemas/LabelValue" } @@ -21858,13 +26152,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -21917,13 +26211,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -21965,13 +26259,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -22029,13 +26323,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -22052,6 +26346,9 @@ "deviations": { "type": "string" }, + "persistentDeny": { + "type": "string" + }, "reason": { "type": "string" }, @@ -22094,13 +26391,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -22150,13 +26447,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -22218,13 +26515,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -22274,13 +26571,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -22331,13 +26628,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -22387,13 +26684,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -22435,19 +26732,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "CIPP.AppSettings.Read", + "x-cipp-role": "CIPP.Core.Read", "parameters": [ { "name": "LocalVersion", @@ -22462,7 +26759,7 @@ }, "/api/ListAPDevices": { "get": { - "summary": "ListAPDevices", + "summary": "Lists Windows Autopilot device identities registered in a tenant.", "tags": [ "Endpoint > Autopilot" ], @@ -22483,18 +26780,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists Windows Autopilot device identities registered in a tenant.", "x-cipp-role": "Endpoint.Autopilot.Read", "parameters": [ { @@ -22503,11 +26801,11 @@ ] } }, - "/api/ListAdminPortalLicenses": { + "/api/ListActiveSyncDevices": { "get": { - "summary": "ListAdminPortalLicenses", + "summary": "Lists all ActiveSync mobile devices registered across Exchange Online mailboxes in a tenant.", "tags": [ - "CIPP > Core" + "Email-Exchange > Reports" ], "security": [ { @@ -22526,19 +26824,20 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "CIPP.Core.Read", + "description": "Lists all ActiveSync mobile devices registered across Exchange Online mailboxes in a tenant.", + "x-cipp-role": "Exchange.Mailbox.Read", "parameters": [ { "$ref": "#/components/parameters/tenantFilter" @@ -22546,85 +26845,11 @@ ] } }, - "/api/ListAlertsQueue": { - "get": { - "summary": "ListAlertsQueue", - "tags": [ - "Tenant > Administration > Alerts" - ], - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StandardResults" - } - } - } - }, - "400": { - "description": "Bad request — missing required field or invalid input" - }, - "401": { - "description": "Unauthorized — invalid or missing bearer token" - }, - "403": { - "description": "Forbidden — caller lacks the required RBAC role" - }, - "500": { - "description": "Internal server error" - } - }, - "x-cipp-role": "CIPP.Alert.Read", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "preset": { - "type": "string" - }, - "logbook": { - "type": "string" - }, - "Actions": { - "type": "string" - }, - "AlertComment": { - "type": "string" - }, - "command": { - "type": "string" - }, - "recurrence": { - "type": "string" - }, - "startDateTime": { - "type": "string" - }, - "postExecution": { - "type": "string" - } - } - } - } - } - } - } - }, - "/api/ListAllTenantDeviceCompliance": { + "/api/ListAdminPortalLicenses": { "get": { - "summary": "ListAllTenantDeviceCompliance", + "summary": "Retrieves license information from the Microsoft 365 Admin Portal for a tenant, including low-friction trial allotments.", "tags": [ - "Uncategorized" + "CIPP > Core" ], "security": [ { @@ -22643,19 +26868,20 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Tenant.DeviceCompliance.Read", + "description": "Retrieves license information from the Microsoft 365 Admin Portal for a tenant, including low-friction trial allotments.", + "x-cipp-role": "CIPP.Core.Read", "parameters": [ { "$ref": "#/components/parameters/tenantFilter" @@ -22663,11 +26889,11 @@ ] } }, - "/api/ListAntiPhishingFilters": { + "/api/ListAlertsQueue": { "get": { - "summary": "ListAntiPhishingFilters", + "summary": "Lists configured alert rules including webhook rules and scheduled alert tasks, showing their configuration and tenan...", "tags": [ - "Email-Exchange > Reports" + "Tenant > Administration > Alerts" ], "security": [ { @@ -22686,69 +26912,74 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Exchange.SpamFilter.Read", - "parameters": [ - { - "$ref": "#/components/parameters/tenantFilter" - } - ] - } - }, - "/api/ListApiTest": { - "get": { - "summary": "ListApiTest", - "tags": [ - "CIPP > Core" - ], - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StandardResults" + "description": "Lists configured alert rules including webhook rules and scheduled alert tasks, showing their configuration and tenant scope.", + "x-cipp-role": "CIPP.Alert.Read", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "CustomSubject": { + "type": "string" + }, + "preset": { + "type": "string", + "description": "(auto) detected in frontend form/call" + }, + "logbook": { + "type": "string", + "description": "(auto) detected in frontend form/call" + }, + "Actions": { + "type": "string", + "description": "(auto) detected in frontend form/call" + }, + "AlertComment": { + "type": "string", + "description": "(auto) detected in frontend form/call" + }, + "command": { + "type": "string", + "description": "(auto) detected in frontend form/call" + }, + "recurrence": { + "type": "string", + "description": "(auto) detected in frontend form/call" + }, + "startDateTime": { + "type": "string", + "description": "(auto) detected in frontend form/call" + }, + "postExecution": { + "type": "string", + "description": "(auto) detected in frontend form/call" + } } } } - }, - "400": { - "description": "Bad request — missing required field or invalid input" - }, - "401": { - "description": "Unauthorized — invalid or missing bearer token" - }, - "403": { - "description": "Forbidden — caller lacks the required RBAC role" - }, - "500": { - "description": "Internal server error" } - }, - "x-cipp-role": "CIPP.Core.Read" + } } }, - "/api/ListAppApprovalTemplates": { + "/api/ListAllTenantDeviceCompliance": { "get": { - "summary": "ListAppApprovalTemplates", + "summary": "Lists device compliance summary across all managed tenants using the Lighthouse managedDeviceCompliances API, or for...", "tags": [ - "Tenant > Administration > Application Approval" + "Uncategorized" ], "security": [ { @@ -22767,24 +26998,214 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists device compliance summary across all managed tenants using the Lighthouse managedDeviceCompliances API, or for a single tenant via Intune.", + "x-cipp-role": "Tenant.DeviceCompliance.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListAndroidEnrollmentProfiles": { + "post": { + "summary": "Lists Android Enterprise enrollment profiles and hydrates token fields when Graph omits them from the list response.", + "tags": [ + "Endpoint > MEM" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Lists Android Enterprise enrollment profiles and hydrates token fields when Graph omits them from the list response.", + "x-cipp-role": "Endpoint.Autopilot.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/ListAntiPhishingFilters": { + "get": { + "summary": "Lists anti-phishing policies and their associated rules configured in Exchange Online Protection.", + "tags": [ + "Email-Exchange > Reports" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Lists anti-phishing policies and their associated rules configured in Exchange Online Protection.", + "x-cipp-role": "Exchange.SpamFilter.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListApiTest": { + "get": { + "summary": "Returns debug information about the current API request, trigger metadata, and environment variables.", + "tags": [ + "CIPP > Core" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Returns debug information about the current API request, trigger metadata, and environment variables. Used for CIPP platform diagnostics.", + "x-cipp-role": "CIPP.Core.Read" + } + }, + "/api/ListAppApprovalTemplates": { + "get": { + "summary": "Lists saved application approval templates for standardized app consent configurations.", + "tags": [ + "Tenant > Administration > Application Approval" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Lists saved application approval templates for standardized app consent configurations.", "x-cipp-role": "Tenant.Application.Read" } }, "/api/ListAppConsentRequests": { "get": { - "summary": "ListAppConsentRequests", + "summary": "Lists pending application consent requests in a tenant, filterable by request status.", "tags": [ "Tenant > Administration" ], @@ -22805,18 +27226,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists pending application consent requests in a tenant, filterable by request status.", "x-cipp-role": "Tenant.Administration.Read", "parameters": [ { @@ -22843,7 +27265,7 @@ }, "/api/ListAppProtectionPolicies": { "get": { - "summary": "ListAppProtectionPolicies", + "summary": "Lists Intune app protection policies for a tenant.", "tags": [ "Endpoint > MEM" ], @@ -22864,29 +27286,38 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists Intune app protection policies for a tenant. Supports UseReportDB=true query parameter to retrieve cached data from the reporting database for significantly better performance, especially when querying AllTenants.", "x-cipp-role": "Endpoint.MEM.Read", "parameters": [ { "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "UseReportDB", + "in": "query", + "required": false, + "schema": { + "type": "string" + } } ] } }, "/api/ListAppStatus": { "get": { - "summary": "ListAppStatus", + "summary": "Lists Intune application installation status across devices for a specific application, including install state and e...", "tags": [ "Uncategorized" ], @@ -22907,18 +27338,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists Intune application installation status across devices for a specific application, including install state and error codes.", "x-cipp-role": "Endpoint.Device.Read", "parameters": [ { @@ -22935,9 +27367,119 @@ ] } }, + "/api/ListAppTemplates": { + "get": { + "summary": "ListAppTemplates", + "tags": [ + "Endpoint > Applications" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.Application.Read", + "parameters": [ + { + "name": "ID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/ListAppleEnrollmentProfiles": { + "post": { + "summary": "Lists Apple Automated Device Enrollment tokens and enrollment profiles.", + "tags": [ + "Endpoint > MEM" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Lists Apple Automated Device Enrollment tokens and enrollment profiles.", + "x-cipp-role": "Endpoint.Autopilot.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, "/api/ListApplicationQueue": { "get": { - "summary": "ListApplicationQueue", + "summary": "Lists queued Intune application deployments that are pending assignment to tenants.", "tags": [ "Endpoint > Applications" ], @@ -22958,24 +27500,25 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists queued Intune application deployments that are pending assignment to tenants.", "x-cipp-role": "Endpoint.Application.Read" } }, "/api/ListApps": { "get": { - "summary": "ListApps", + "summary": "Lists Intune managed applications for a tenant.", "tags": [ "Endpoint > Applications" ], @@ -22996,29 +27539,38 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists Intune managed applications for a tenant. Supports UseReportDB=true query parameter to retrieve cached data from the reporting database for significantly better performance, especially when querying AllTenants.", "x-cipp-role": "Endpoint.Application.Read", "parameters": [ { "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "UseReportDB", + "in": "query", + "required": false, + "schema": { + "type": "string" + } } ] } }, "/api/ListAppsRepository": { "post": { - "summary": "ListAppsRepository", + "summary": "Searches external application repositories (WinGet, Chocolatey, etc.) for available application packages.", "tags": [ "Endpoint > Applications" ], @@ -23039,18 +27591,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Searches external application repositories (WinGet, Chocolatey, etc.) for available application packages.", "x-cipp-role": "Endpoint.Application.Read", "requestBody": { "required": true, @@ -23102,6 +27655,9 @@ "detectionPath": { "type": "string" }, + "detectionScript": { + "type": "string" + }, "DisableRestart": { "type": "boolean" }, @@ -23117,6 +27673,9 @@ "$ref": "#/components/schemas/LabelValue" } }, + "excludeGroup": { + "type": "string" + }, "InstallAsSystem": { "type": "boolean" }, @@ -23164,6 +27723,12 @@ "SharedComputerActivation": { "type": "boolean" }, + "templateDescription": { + "type": "string" + }, + "templateName": { + "type": "string" + }, "uninstallScript": { "type": "string" }, @@ -23173,11 +27738,16 @@ "useCustomXml": { "type": "boolean" }, + "useDetectionScript": { + "type": "boolean" + }, "Search": { - "type": "string" + "type": "string", + "description": "(auto) API uses 'Search', frontend sends 'searchQuery' \u2014 likely the same field (similarity=0.71). Verify and correct type." }, "Repository": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: Repository" } } } @@ -23209,13 +27779,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -23236,7 +27806,7 @@ }, "/api/ListAssignmentFilters": { "get": { - "summary": "ListAssignmentFilters", + "summary": "Lists Intune assignment filters for a tenant.", "tags": [ "Endpoint > MEM" ], @@ -23257,18 +27827,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists Intune assignment filters for a tenant. Supports UseReportDB=true query parameter to retrieve cached data from the reporting database for significantly better performance, especially when querying AllTenants.", "x-cipp-role": "Endpoint.MEM.Read", "parameters": [ { @@ -23281,13 +27852,21 @@ }, { "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "UseReportDB", + "in": "query", + "required": false, + "schema": { + "type": "string" + } } ] } }, "/api/ListAuditLogSearches": { "get": { - "summary": "ListAuditLogSearches", + "summary": "Lists or creates Microsoft 365 Unified Audit Log searches for a tenant, with support for retrieving search results.", "tags": [ "Tenant > Administration > Alerts" ], @@ -23308,18 +27887,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists or creates Microsoft 365 Unified Audit Log searches for a tenant, with support for retrieving search results.", "x-cipp-role": "Tenant.Alert.Read", "parameters": [ { @@ -23354,7 +27934,7 @@ }, "/api/ListAuditLogTest": { "get": { - "summary": "ListAuditLogTest", + "summary": "Tests audit log webhook rules against a specific search to validate rule matching and alert triggering.", "tags": [ "Tenant > Administration > Alerts" ], @@ -23375,18 +27955,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Tests audit log webhook rules against a specific search to validate rule matching and alert triggering.", "x-cipp-role": "Tenant.Alert.Read", "parameters": [ { @@ -23405,7 +27986,7 @@ }, "/api/ListAuditLogs": { "get": { - "summary": "ListAuditLogs", + "summary": "Lists audit log entries from the CIPP audit log store, filterable by tenant, date range, and log ID.", "tags": [ "Tenant > Administration > Alerts" ], @@ -23426,18 +28007,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists audit log entries from the CIPP audit log store, filterable by tenant, date range, and log ID.", "x-cipp-role": "CIPP.Alert.Read", "parameters": [ { @@ -23480,7 +28062,7 @@ }, "/api/ListAutopilotconfig": { "get": { - "summary": "ListAutopilotconfig", + "summary": "Lists Windows Autopilot deployment profiles, Enrollment Status Page configurations, or Windows Hello for Business pol...", "tags": [ "Endpoint > Autopilot" ], @@ -23501,18 +28083,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists Windows Autopilot deployment profiles, Enrollment Status Page configurations, or Windows Hello for Business policies for a tenant.", "x-cipp-role": "Endpoint.Autopilot.Read", "parameters": [ { @@ -23552,13 +28135,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -23573,10 +28156,12 @@ "type": "object", "properties": { "name": { - "type": "string" + "type": "string", + "description": "(auto) detected in frontend form/call" }, "description": { - "type": "string" + "type": "string", + "description": "(auto) detected in frontend form/call" } } } @@ -23587,7 +28172,7 @@ }, "/api/ListAzureADConnectStatus": { "get": { - "summary": "ListAzureADConnectStatus", + "summary": "Retrieves Entra ID Connect (Azure AD Connect) synchronization status and configuration for a tenant, including sync i...", "tags": [ "Identity > Reports" ], @@ -23608,18 +28193,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Retrieves Entra ID Connect (Azure AD Connect) synchronization status and configuration for a tenant, including sync intervals, password sync, and pass-through authentication settings.", "x-cipp-role": "Tenant.Directory.Read", "parameters": [ { @@ -23638,7 +28224,7 @@ }, "/api/ListBPA": { "get": { - "summary": "ListBPA", + "summary": "Lists Best Practice Analyser (BPA) report results for tenants, based on a selected BPA template.", "tags": [ "Tenant > Standards" ], @@ -23659,18 +28245,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists Best Practice Analyser (BPA) report results for tenants, based on a selected BPA template.", "x-cipp-role": "Tenant.BestPracticeAnalyser.Read", "parameters": [ { @@ -23689,7 +28276,7 @@ }, "/api/ListBPATemplates": { "get": { - "summary": "ListBPATemplates", + "summary": "Lists available Best Practice Analyser (BPA) templates that define which checks to run.", "tags": [ "Tenant > Standards" ], @@ -23710,18 +28297,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists available Best Practice Analyser (BPA) templates that define which checks to run.", "x-cipp-role": "Tenant.BestPracticeAnalyser.Read", "parameters": [ { @@ -23737,7 +28325,7 @@ }, "/api/ListBasicAuth": { "get": { - "summary": "ListBasicAuth", + "summary": "Lists sign-in events using basic authentication (legacy protocols) for a tenant from the audit logs.", "tags": [ "Identity > Reports" ], @@ -23758,18 +28346,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists sign-in events using basic authentication (legacy protocols) for a tenant from the audit logs.", "x-cipp-role": "Identity.AuditLog.Read", "parameters": [ { @@ -23780,7 +28369,7 @@ }, "/api/ListBreachesAccount": { "get": { - "summary": "ListBreachesAccount", + "summary": "Checks an email account or domain against the Have I Been Pwned (HIBP) breach database.", "tags": [ "Uncategorized" ], @@ -23801,18 +28390,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Checks an email account or domain against the Have I Been Pwned (HIBP) breach database.", "x-cipp-role": "CIPP.Core.Read", "parameters": [ { @@ -23828,7 +28418,7 @@ }, "/api/ListBreachesTenant": { "get": { - "summary": "ListBreachesTenant", + "summary": "Lists cached data breach results for user accounts in a tenant from the CIPP breach monitoring cache.", "tags": [ "Uncategorized" ], @@ -23849,18 +28439,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists cached data breach results for user accounts in a tenant from the CIPP breach monitoring cache.", "x-cipp-role": "CIPP.Core.Read", "parameters": [ { @@ -23871,7 +28462,7 @@ }, "/api/ListCAtemplates": { "get": { - "summary": "ListCAtemplates", + "summary": "Lists saved Conditional Access policy templates for deploying standardized CA configurations.", "tags": [ "Tenant > Conditional" ], @@ -23892,42 +28483,113 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists saved Conditional Access policy templates for deploying standardized CA configurations.", "x-cipp-role": "Tenant.ConditionalAccess.Read", "parameters": [ { - "name": "GUID", + "name": "mode", "in": "query", "required": false, "schema": { "type": "string" } }, + { + "name": "GUID", + "in": "query", + "required": false, + "schema": { + "type": "string", + "description": "GUID parameter" + } + }, { "name": "ID", "in": "query", "required": false, "schema": { - "type": "string" + "type": "string", + "description": "Query parameter from PS1: ID" } } ] } }, + "/api/ListCIPPUsers": { + "get": { + "summary": "Lists CIPP platform users and their role assignments from the allowedUsers table.", + "tags": [ + "CIPP > Settings" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Lists CIPP platform users and their role assignments from the allowedUsers table. Requires SuperAdmin access.", + "x-cipp-role": "CIPP.SuperAdmin.Read", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Roles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LabelValue" + } + }, + "UPN": { + "type": "string" + } + } + } + } + } + } + } + }, "/api/ListCSPLicenses": { "get": { - "summary": "ListCSPLicenses", + "summary": "Lists CSP (Cloud Solution Provider) license subscriptions for a tenant via the Sherweb integration.", "tags": [ "Uncategorized" ], @@ -23948,18 +28610,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists CSP (Cloud Solution Provider) license subscriptions for a tenant via the Sherweb integration.", "x-cipp-role": "Tenant.Directory.Read", "parameters": [ { @@ -23970,7 +28633,7 @@ }, "/api/ListCSPsku": { "get": { - "summary": "ListCSPsku", + "summary": "Lists available CSP SKUs and current subscriptions for a tenant via the Sherweb integration.", "tags": [ "Uncategorized" ], @@ -23991,18 +28654,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists available CSP SKUs and current subscriptions for a tenant via the Sherweb integration.", "x-cipp-role": "Tenant.Directory.Read", "parameters": [ { @@ -24021,7 +28685,7 @@ }, "/api/ListCalendarPermissions": { "get": { - "summary": "ListCalendarPermissions", + "summary": "Lists calendar permissions for mailboxes in a tenant.", "tags": [ "Email-Exchange > Administration" ], @@ -24042,18 +28706,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists calendar permissions for mailboxes in a tenant. Supports UseReportDB=true query parameter to retrieve cached data from the reporting database for significantly better performance, especially when querying AllTenants.", "x-cipp-role": "Exchange.Mailbox.Read", "parameters": [ { @@ -24088,7 +28753,7 @@ }, "/api/ListCheckExtAlerts": { "get": { - "summary": "ListCheckExtAlerts", + "summary": "Lists extension alert check results from third-party integrations for a tenant.", "tags": [ "Uncategorized" ], @@ -24109,18 +28774,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists extension alert check results from third-party integrations for a tenant.", "x-cipp-role": "CIPP.Core.Read", "parameters": [ { @@ -24129,6 +28795,45 @@ ] } }, + "/api/ListCippQueue": { + "get": { + "summary": "Lists active and recent CIPP background processing queue items and their status.", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Lists active and recent CIPP background processing queue items and their status.", + "x-cipp-role": "CIPP.Core.Read" + } + }, "/api/ListCommunityRepos": { "get": { "summary": "List community repositories in Table Storage", @@ -24152,19 +28857,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "description": "This function lists community repositories in Table Storage\n .FUNCTIONALITY\n Entrypoint,AnyTenant\n .ROLE\n CIPP.Core.Read\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", + "description": "This function lists community repositories in Table Storage", "x-cipp-role": "CIPP.Core.Read", "parameters": [ { @@ -24180,7 +28885,7 @@ }, "/api/ListCompliancePolicies": { "get": { - "summary": "ListCompliancePolicies", + "summary": "Lists Intune device compliance policies for a tenant.", "tags": [ "Endpoint > MEM" ], @@ -24201,29 +28906,38 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists Intune device compliance policies for a tenant. Supports UseReportDB=true query parameter to retrieve cached data from the reporting database for significantly better performance, especially when querying AllTenants.", "x-cipp-role": "Endpoint.MEM.Read", "parameters": [ { "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "UseReportDB", + "in": "query", + "required": false, + "schema": { + "type": "string" + } } ] } }, "/api/ListConditionalAccessPolicies": { "get": { - "summary": "ListConditionalAccessPolicies", + "summary": "Lists Conditional Access policies for a tenant with resolved display names for users, groups, applications, and locat...", "tags": [ "Tenant > Conditional" ], @@ -24244,18 +28958,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists Conditional Access policies for a tenant with resolved display names for users, groups, applications, and locations.", "x-cipp-role": "Tenant.ConditionalAccess.Read", "parameters": [ { @@ -24266,7 +28981,7 @@ }, "/api/ListConditionalAccessPolicyChanges": { "get": { - "summary": "ListConditionalAccessPolicyChanges", + "summary": "Lists audit log entries showing changes to a specific Conditional Access policy, including before/after values.", "tags": [ "Tenant > Conditional" ], @@ -24287,18 +29002,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists audit log entries showing changes to a specific Conditional Access policy, including before/after values.", "x-cipp-role": "Tenant.ConditionalAccess.Read", "parameters": [ { @@ -24325,7 +29041,7 @@ }, "/api/ListConnectionFilter": { "get": { - "summary": "ListConnectionFilter", + "summary": "Lists hosted connection filter policies (IP allow/block lists) in Exchange Online Protection.", "tags": [ "Email-Exchange > Spamfilter" ], @@ -24346,18 +29062,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists hosted connection filter policies (IP allow/block lists) in Exchange Online Protection.", "x-cipp-role": "Exchange.ConnectionFilter.Read", "x-cipp-dynamic-options": true, "parameters": [ @@ -24369,7 +29086,7 @@ }, "/api/ListConnectionFilterTemplates": { "get": { - "summary": "ListConnectionFilterTemplates", + "summary": "Lists saved connection filter policy templates for Exchange Online Protection.", "tags": [ "Email-Exchange > Spamfilter" ], @@ -24390,18 +29107,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists saved connection filter policy templates for Exchange Online Protection.", "x-cipp-role": "Exchange.ConnectionFilter.Read", "parameters": [ { @@ -24417,7 +29135,7 @@ }, "/api/ListContactPermissions": { "get": { - "summary": "ListContactPermissions", + "summary": "Lists folder-level permissions on a user's Contacts folder in Exchange Online.", "tags": [ "Email-Exchange > Administration" ], @@ -24438,18 +29156,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists folder-level permissions on a user's Contacts folder in Exchange Online.", "x-cipp-role": "Exchange.Mailbox.Read", "parameters": [ { @@ -24468,7 +29187,7 @@ }, "/api/ListContactTemplates": { "get": { - "summary": "ListContactTemplates", + "summary": "Lists saved mail contact templates used for creating new Exchange contacts.", "tags": [ "Email-Exchange > Administration > Contacts" ], @@ -24489,18 +29208,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists saved mail contact templates used for creating new Exchange contacts.", "x-cipp-role": "Exchange.Contact.Read", "parameters": [ { @@ -24516,7 +29236,7 @@ }, "/api/ListContacts": { "get": { - "summary": "ListContacts", + "summary": "Lists Exchange Online mail contacts for a tenant, with optional filtering by contact ID for detailed view.", "tags": [ "Email-Exchange > Administration > Contacts" ], @@ -24537,18 +29257,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists Exchange Online mail contacts for a tenant, with optional filtering by contact ID for detailed view.", "x-cipp-role": "Exchange.Contact.Read", "parameters": [ { @@ -24565,11 +29286,11 @@ ] } }, - "/api/ListCustomDataMappings": { + "/api/ListContainerLogs": { "get": { - "summary": "ListCustomDataMappings", + "summary": "Retrieves container logs for the CIPP backend process.", "tags": [ - "CIPP > Core" + "CIPP > Settings" ], "security": [ { @@ -24588,22 +29309,23 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "CIPP.Core.Read", + "description": "Retrieves container logs for the CIPP backend process. Requires SuperAdmin access. Supports ReadLog, ListFiles, and snapshot actions.", + "x-cipp-role": "CIPP.SuperAdmin.Read", "parameters": [ { - "name": "directoryObject", + "name": "Action", "in": "query", "required": false, "schema": { @@ -24611,7 +29333,7 @@ } }, { - "name": "sourceType", + "name": "Exclude", "in": "query", "required": false, "schema": { @@ -24619,54 +29341,77 @@ } }, { - "$ref": "#/components/parameters/tenantFilter" - } - ] - } - }, - "/api/ListCustomRole": { - "get": { - "summary": "ListCustomRole", - "tags": [ - "CIPP > Settings" - ], - "security": [ + "name": "File", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StandardResults" - } - } + "name": "From", + "in": "query", + "required": false, + "schema": { + "type": "string" } }, - "400": { - "description": "Bad request — missing required field or invalid input" + { + "name": "Level", + "in": "query", + "required": false, + "schema": { + "type": "string" + } }, - "401": { - "description": "Unauthorized — invalid or missing bearer token" + { + "name": "Regex", + "in": "query", + "required": false, + "schema": { + "type": "string" + } }, - "403": { - "description": "Forbidden — caller lacks the required RBAC role" + { + "name": "Search", + "in": "query", + "required": false, + "schema": { + "type": "string" + } }, - "500": { - "description": "Internal server error" + { + "name": "SortDesc", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Tail", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "To", + "in": "query", + "required": false, + "schema": { + "type": "string" + } } - }, - "x-cipp-role": "CIPP.Core.Read" + ] } }, - "/api/ListCustomVariables": { + "/api/ListCustomDataMappings": { "get": { - "summary": "ListCustomVariables", + "summary": "Lists custom data mappings that define how external data sources map to CIPP directory objects, filterable by source...", "tags": [ - "CIPP > Settings" + "CIPP > Core" ], "security": [ { @@ -24685,22 +29430,23 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists custom data mappings that define how external data sources map to CIPP directory objects, filterable by source type, directory object, and tenant.", "x-cipp-role": "CIPP.Core.Read", "parameters": [ { - "name": "excludeGlobalReserved", + "name": "directoryObject", "in": "query", "required": false, "schema": { @@ -24708,7 +29454,7 @@ } }, { - "name": "includeSystem", + "name": "sourceType", "in": "query", "required": false, "schema": { @@ -24721,11 +29467,11 @@ ] } }, - "/api/ListDBCache": { + "/api/ListCustomRole": { "get": { - "summary": "ListDBCache", + "summary": "Lists custom RBAC roles defined in CIPP, including their permission sets and associated access role groups.", "tags": [ - "Uncategorized" + "CIPP > Settings" ], "security": [ { @@ -24744,39 +29490,27 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "CIPP.Core.Read", - "parameters": [ - { - "$ref": "#/components/parameters/tenantFilter" - }, - { - "name": "type", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - } - ] + "description": "Lists custom RBAC roles defined in CIPP, including their permission sets and associated access role groups.", + "x-cipp-role": "CIPP.Core.Read" } }, - "/api/ListDefenderState": { - "get": { - "summary": "ListDefenderState", + "/api/ListCustomScripts": { + "post": { + "summary": "Lists custom PowerShell scripts stored in CIPP, with optional filtering by script GUID and version history.", "tags": [ - "Endpoint > MEM" + "Tools > Custom-Scripts" ], "security": [ { @@ -24795,22 +29529,23 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Endpoint.MEM.Read", + "description": "Lists custom PowerShell scripts stored in CIPP, with optional filtering by script GUID and version history.", + "x-cipp-role": "CIPP.Tests.Read", "parameters": [ { - "name": "DeviceID", + "name": "IncludeAllVersions", "in": "query", "required": false, "schema": { @@ -24818,16 +29553,39 @@ } }, { - "$ref": "#/components/parameters/tenantFilter" + "name": "ScriptGuid", + "in": "query", + "required": false, + "schema": { + "type": "string" + } } - ] + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "IncludeAllVersions": { + "type": "string" + }, + "ScriptGuid": { + "type": "string" + } + } + } + } + } + } } }, - "/api/ListDefenderTVM": { + "/api/ListCustomVariables": { "get": { - "summary": "ListDefenderTVM", + "summary": "Lists custom variables configured in CIPP for use in templates and automation.", "tags": [ - "Endpoint > MEM" + "CIPP > Settings" ], "security": [ { @@ -24846,18 +29604,183 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists custom variables configured in CIPP for use in templates and automation.", + "x-cipp-role": "CIPP.Core.Read", + "parameters": [ + { + "name": "excludeGlobalReserved", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "includeSystem", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListDBCache": { + "get": { + "summary": "Retrieves cached tenant data from the CIPP reporting database (CippReportingDB).", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Retrieves cached tenant data from the CIPP reporting database (CippReportingDB). This is the fastest\n and most efficient way to query tenant data across single or multiple tenants. The database is populated\n nightly by background cache jobs, so data is typically at most 24 hours old.\n\n Required query parameters:\n - tenantFilter: The tenant domain or 'AllTenants' to query all managed tenants.\n - type: The cache collection to retrieve (e.g. Users, Groups, Mailboxes, Devices, etc.).\n\n Use type=_availableTypes to discover which cache collections exist for a given tenant. Omitting the\n type parameter also returns the available types.\n\n PERFORMANCE GUIDANCE: For AllTenants queries or any bulk/cross-tenant data retrieval, prefer\n ListDBCache over calling individual endpoints (e.g. ListUsers, ListGroups, ListMailboxes) directly.\n Individual endpoints make live API calls per tenant which is significantly slower and may hit\n throttling limits. ListDBCache reads pre-cached data from Azure Table Storage and returns results\n in seconds regardless of tenant count.\n\n Recommended workflow for MCP tool selection:\n 1. Call ListDBCache with type=_availableTypes to discover available cache collections.\n 2. If the data you need exists as a cache type, use ListDBCache with that type.\n 3. Only fall back to individual List* endpoints when you need real-time data for a single tenant\n or when the data is not available in the cache.\n\n Common cache types include: Users, Groups, Mailboxes, Devices, ConditionalAccess, Applications,\n IntunePolicy, CompliancePolicy, and many more. The exact set depends on what has been configured.", + "x-cipp-role": "CIPP.Core.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "type", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/ListDefenderState": { + "get": { + "summary": "Lists Microsoft Defender antivirus state and detected threats for Intune-managed devices in a tenant.", + "tags": [ + "Endpoint > MEM" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Lists Microsoft Defender antivirus state and detected threats for Intune-managed devices in a tenant.", + "x-cipp-role": "Endpoint.MEM.Read", + "parameters": [ + { + "name": "DeviceID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListDefenderTVM": { + "get": { + "summary": "Lists software vulnerabilities from Microsoft Defender Threat and Vulnerability Management (TVM), grouped by CVE ID.", + "tags": [ + "Endpoint > MEM" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Lists software vulnerabilities from Microsoft Defender Threat and Vulnerability Management (TVM), grouped by CVE ID.", "x-cipp-role": "Endpoint.MEM.Read", "parameters": [ { @@ -24868,7 +29791,7 @@ }, "/api/ListDeletedItems": { "get": { - "summary": "ListDeletedItems", + "summary": "Lists soft-deleted directory objects in Entra ID, including users, groups, applications, service principals, and admi...", "tags": [ "Identity > Administration > Users" ], @@ -24889,18 +29812,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists soft-deleted directory objects in Entra ID, including users, groups, applications, service principals, and administrative units.", "x-cipp-role": "Tenant.Directory.Read", "parameters": [ { @@ -24911,7 +29835,7 @@ }, "/api/ListDetectedAppDevices": { "get": { - "summary": "ListDetectedAppDevices", + "summary": "Lists Intune-managed devices that have a specific detected application installed, identified by App ID.", "tags": [ "Uncategorized" ], @@ -24932,18 +29856,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists Intune-managed devices that have a specific detected application installed, identified by App ID.", "x-cipp-role": "Identity.Device.Read", "parameters": [ { @@ -24962,7 +29887,7 @@ }, "/api/ListDetectedApps": { "get": { - "summary": "ListDetectedApps", + "summary": "Lists applications detected on Intune-managed devices in a tenant, optionally including device associations.", "tags": [ "Uncategorized" ], @@ -24983,18 +29908,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists applications detected on Intune-managed devices in a tenant, optionally including device associations.", "x-cipp-role": "Identity.Device.Read", "parameters": [ { @@ -25021,7 +29947,7 @@ }, "/api/ListDeviceDetails": { "get": { - "summary": "ListDeviceDetails", + "summary": "Retrieves detailed information about a specific Intune-managed device by ID, name, or serial number.", "tags": [ "Uncategorized" ], @@ -25042,18 +29968,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Retrieves detailed information about a specific Intune-managed device by ID, name, or serial number.", "x-cipp-role": "Identity.Device.Read", "parameters": [ { @@ -25088,7 +30015,7 @@ }, "/api/ListDevices": { "get": { - "summary": "ListDevices", + "summary": "Lists Intune-managed devices for a tenant, including device compliance and configuration status.", "tags": [ "Endpoint > Reports" ], @@ -25109,18 +30036,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists Intune-managed devices for a tenant, including device compliance and configuration status.", "x-cipp-role": "Endpoint.Device.Read", "parameters": [ { @@ -25131,7 +30059,7 @@ }, "/api/ListDiagnosticsPresets": { "get": { - "summary": "ListDiagnosticsPresets", + "summary": "Lists saved diagnostics presets used for troubleshooting CIPP configuration.", "tags": [ "CIPP > Core" ], @@ -25152,24 +30080,25 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists saved diagnostics presets used for troubleshooting CIPP configuration. Requires SuperAdmin access.", "x-cipp-role": "CIPP.SuperAdmin.Read" } }, "/api/ListDirectoryObjects": { "post": { - "summary": "ListDirectoryObjects", + "summary": "Resolves Entra ID directory objects by their IDs using the directoryObjects/getByIds batch endpoint.", "tags": [ "CIPP > Core" ], @@ -25190,18 +30119,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Resolves Entra ID directory objects by their IDs using the directoryObjects/getByIds batch endpoint. Accepts an array of object IDs in the request body.", "x-cipp-role": "CIPP.Core.Read", "requestBody": { "required": true, @@ -25232,11 +30162,11 @@ } } }, - "/api/ListDomainAnalyser": { + "/api/ListDlpCompliancePolicy": { "get": { - "summary": "ListDomainAnalyser", + "summary": "Lists Data Loss Prevention (DLP) compliance policies and their associated rules from the Security & Compliance Center.", "tags": [ - "Tenant > Standards" + "Security > Compliance-DLP" ], "security": [ { @@ -25255,19 +30185,21 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Tenant.DomainAnalyser.Read", + "description": "Lists Data Loss Prevention (DLP) compliance policies and their associated rules from the Security & Compliance Center.", + "x-cipp-role": "Security.DlpCompliancePolicy.Read", + "x-cipp-dynamic-options": true, "parameters": [ { "$ref": "#/components/parameters/tenantFilter" @@ -25275,11 +30207,11 @@ ] } }, - "/api/ListDomainHealth": { + "/api/ListDlpCompliancePolicyTemplates": { "get": { - "summary": "ListDomainHealth", + "summary": "Lists saved DLP compliance policy templates for deploying standardized DLP configurations.", "tags": [ - "Tenant > Standards" + "Security > Compliance-DLP" ], "security": [ { @@ -25298,62 +30230,23 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Tenant.DomainAnalyser.Read", + "description": "Lists saved DLP compliance policy templates for deploying standardized DLP configurations.", + "x-cipp-role": "Security.DlpCompliancePolicy.Read", "parameters": [ { - "name": "Action", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "Domain", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "ExpectedInclude", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "Record", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "Selector", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "Subdomains", + "name": "ID", "in": "query", "required": false, "schema": { @@ -25363,11 +30256,11 @@ ] } }, - "/api/ListDomains": { + "/api/ListDomainAnalyser": { "get": { - "summary": "ListDomains", + "summary": "Lists domain analysis results (SPF, DKIM, DMARC, DNSSEC) for tenant domains.", "tags": [ - "Tenant > Administration" + "Tenant > Standards" ], "security": [ { @@ -25386,19 +30279,20 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Tenant.Administration.Read", + "description": "Lists domain analysis results (SPF, DKIM, DMARC, DNSSEC) for tenant domains.", + "x-cipp-role": "Tenant.DomainAnalyser.Read", "parameters": [ { "$ref": "#/components/parameters/tenantFilter" @@ -25406,11 +30300,11 @@ ] } }, - "/api/ListEquipment": { + "/api/ListDomainHealth": { "get": { - "summary": "ListEquipment", + "summary": "Performs real-time DNS health checks (MX, SPF, DMARC, DKIM, DNSSEC, MTA-STS, HTTPS) for a specific domain.", "tags": [ - "Email-Exchange > Resources" + "Tenant > Standards" ], "security": [ { @@ -25429,22 +30323,23 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Exchange.Equipment.Read", + "description": "Performs real-time DNS health checks (MX, SPF, DMARC, DKIM, DNSSEC, MTA-STS, HTTPS) for a specific domain.", + "x-cipp-role": "Tenant.DomainAnalyser.Read", "parameters": [ { - "name": "EquipmentId", + "name": "Action", "in": "query", "required": false, "schema": { @@ -25452,16 +30347,61 @@ } }, { - "$ref": "#/components/parameters/tenantFilter" + "name": "Domain", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "ExpectedInclude", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "ExpectedTarget", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Record", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Selector", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Subdomains", + "in": "query", + "required": false, + "schema": { + "type": "string" + } } ] } }, - "/api/ListExConnectorTemplates": { + "/api/ListDomains": { "get": { - "summary": "ListExConnectorTemplates", + "summary": "Lists verified domains for a tenant, showing default and initial domain status.", "tags": [ - "Email-Exchange > Transport" + "Tenant > Administration" ], "security": [ { @@ -25480,18 +30420,115 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists verified domains for a tenant, showing default and initial domain status.", + "x-cipp-role": "Tenant.Administration.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListEquipment": { + "get": { + "summary": "Lists equipment mailboxes (projectors, vehicles, etc.) in Exchange Online for a tenant.", + "tags": [ + "Email-Exchange > Resources" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Lists equipment mailboxes (projectors, vehicles, etc.) in Exchange Online for a tenant.", + "x-cipp-role": "Exchange.Equipment.Read", + "parameters": [ + { + "name": "EquipmentId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListExConnectorTemplates": { + "get": { + "summary": "Lists saved Exchange connector templates for creating inbound/outbound connectors.", + "tags": [ + "Email-Exchange > Transport" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Lists saved Exchange connector templates for creating inbound/outbound connectors.", "x-cipp-role": "Exchange.Connector.Read", "parameters": [ { @@ -25507,7 +30544,7 @@ }, "/api/ListExchangeConnectors": { "get": { - "summary": "ListExchangeConnectors", + "summary": "Lists inbound and outbound mail flow connectors configured in Exchange Online.", "tags": [ "Email-Exchange > Transport" ], @@ -25528,18 +30565,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists inbound and outbound mail flow connectors configured in Exchange Online.", "x-cipp-role": "Exchange.Connector.Read", "parameters": [ { @@ -25550,7 +30588,7 @@ }, "/api/ListExcludedLicenses": { "get": { - "summary": "ListExcludedLicenses", + "summary": "Lists license SKUs that have been excluded from CIPP license counts and reporting.", "tags": [ "CIPP > Settings" ], @@ -25571,24 +30609,25 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists license SKUs that have been excluded from CIPP license counts and reporting.", "x-cipp-role": "CIPP.AppSettings.Read" } }, "/api/ListExoRequest": { "post": { - "summary": "ListExoRequest", + "summary": "Executes an arbitrary read-only Exchange Online cmdlet (Get-* or Search-*) for a tenant.", "tags": [ "Email-Exchange > Tools" ], @@ -25609,18 +30648,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Executes an arbitrary read-only Exchange Online cmdlet (Get-* or Search-*) for a tenant. Accepts cmdlet name and parameters in the request body.", "x-cipp-role": "CIPP.Core.Read", "requestBody": { "required": true, @@ -25689,19 +30729,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "description": "This function is used to list the extension cache data.\n .FUNCTIONALITY\n Entrypoint\n .ROLE\n CIPP.Core.Read\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)\n $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter\n $DataTypes = $Request.Query.dataTypes -split ',' ?? $Request.Body.dataTypes ?? 'All'", + "description": "This function is used to list the extension cache data.", "x-cipp-role": "CIPP.Core.Read", "parameters": [ { @@ -25741,7 +30781,7 @@ }, "/api/ListExtensionSync": { "get": { - "summary": "ListExtensionSync", + "summary": "Lists extension synchronization tasks and their status, including last sync results for configured third-party integr...", "tags": [ "CIPP > Extensions" ], @@ -25762,24 +30802,25 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists extension synchronization tasks and their status, including last sync results for configured third-party integrations (Hudu, Halo, NinjaOne, etc.).", "x-cipp-role": "CIPP.Extension.Read" } }, "/api/ListExtensionsConfig": { "get": { - "summary": "ListExtensionsConfig", + "summary": "Lists the current CIPP extension configuration, including settings for HaloPSA, NinjaOne, Hudu, and other integrations.", "tags": [ "Uncategorized" ], @@ -25800,18 +30841,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists the current CIPP extension configuration, including settings for HaloPSA, NinjaOne, Hudu, and other integrations.", "x-cipp-role": "CIPP.Extension.Read", "requestBody": { "required": true, @@ -25847,7 +30889,7 @@ }, "/api/ListExternalTenantInfo": { "get": { - "summary": "ListExternalTenantInfo", + "summary": "Looks up publicly available tenant information for an external Entra ID tenant by domain or tenant ID.", "tags": [ "Uncategorized" ], @@ -25868,18 +30910,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Looks up publicly available tenant information for an external Entra ID tenant by domain or tenant ID.", "x-cipp-role": "CIPP.Core.Read", "parameters": [ { @@ -25895,7 +30938,7 @@ }, "/api/ListFeatureFlags": { "get": { - "summary": "ListFeatureFlags", + "summary": "Lists CIPP feature flags and their enabled/disabled state, including environment-driven overrides.", "tags": [ "CIPP > Core" ], @@ -25916,18 +30959,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists CIPP feature flags and their enabled/disabled state, including environment-driven overrides.", "x-cipp-role": "CIPP.Core.Read" } }, @@ -25954,13 +30998,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -25997,7 +31041,7 @@ }, "/api/ListFunctionStats": { "get": { - "summary": "ListFunctionStats", + "summary": "Lists execution statistics for CIPP functions, including timing and performance metrics, filterable by function type...", "tags": [ "Uncategorized" ], @@ -26018,18 +31062,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists execution statistics for CIPP functions, including timing and performance metrics, filterable by function type and time range.", "x-cipp-role": "CIPP.Core.Read", "parameters": [ { @@ -26064,7 +31109,7 @@ }, "/api/ListGDAPAccessAssignments": { "get": { - "summary": "ListGDAPAccessAssignments", + "summary": "Lists GDAP access assignments for a specific delegated admin relationship, including security group members and their...", "tags": [ "Tenant > GDAP" ], @@ -26085,18 +31130,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists GDAP access assignments for a specific delegated admin relationship, including security group members and their role mappings.", "x-cipp-role": "Tenant.Relationship.Read", "parameters": [ { @@ -26110,9 +31156,48 @@ ] } }, + "/api/ListGDAPContracts": { + "get": { + "summary": "Lists Microsoft partner contracts (customer tenant relationships) from the Graph API.", + "tags": [ + "Tenant > GDAP" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Lists Microsoft partner contracts (customer tenant relationships) from the Graph API.", + "x-cipp-role": "Tenant.Relationship.Read" + } + }, "/api/ListGDAPInvite": { "get": { - "summary": "ListGDAPInvite", + "summary": "Lists GDAP relationship invitations and their role mappings, optionally filtered by relationship ID.", "tags": [ "Tenant > GDAP" ], @@ -26133,18 +31218,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists GDAP relationship invitations and their role mappings, optionally filtered by relationship ID.", "x-cipp-role": "Tenant.Relationship.Read", "parameters": [ { @@ -26155,12 +31241,39 @@ "type": "string" } } - ] + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "gdapRoles": { + "$ref": "#/components/schemas/LabelValue" + }, + "id": { + "$ref": "#/components/schemas/LabelValue" + }, + "ignoreMissingRoles": { + "type": "boolean" + }, + "remapRoles": { + "type": "string" + }, + "standardsExcludeAllTenants": { + "type": "boolean" + } + } + } + } + } + } } }, - "/api/ListGDAPRoles": { + "/api/ListGDAPRelationships": { "get": { - "summary": "ListGDAPRoles", + "summary": "Lists GDAP delegated admin relationships with customer tenants, with optional filtering by relationship ID or OData f...", "tags": [ "Tenant > GDAP" ], @@ -26181,26 +31294,64 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Tenant.Relationship.Read" + "description": "Lists GDAP delegated admin relationships with customer tenants, with optional filtering by relationship ID or OData filter.", + "x-cipp-role": "Tenant.Relationship.Read", + "parameters": [ + { + "name": "id", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "gdapRoles": { + "$ref": "#/components/schemas/LabelValue" + }, + "id": { + "$ref": "#/components/schemas/LabelValue" + }, + "ignoreMissingRoles": { + "type": "boolean" + }, + "remapRoles": { + "type": "string" + }, + "standardsExcludeAllTenants": { + "type": "boolean" + } + } + } + } + } + } } }, - "/api/ListGenericTestFunction": { + "/api/ListGDAPRoles": { "get": { - "summary": "ListGenericTestFunction", + "summary": "Lists the configured GDAP role-to-security-group mappings used for delegated admin access.", "tags": [ - "Uncategorized" + "Tenant > GDAP" ], "security": [ { @@ -26219,26 +31370,27 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "CIPP.Core.Read" + "description": "Lists the configured GDAP role-to-security-group mappings used for delegated admin access.", + "x-cipp-role": "Tenant.Relationship.Read" } }, - "/api/ListGitHubReleaseNotes": { + "/api/ListGDAPServicePrincipals": { "get": { - "summary": "Retrieves release notes for a GitHub repository.", + "summary": "Lists service principals in a customer tenant owned by the partner or specified vendor tenant IDs, useful for identif...", "tags": [ - "Tools > GitHub" + "Tenant > GDAP" ], "security": [ { @@ -26257,23 +31409,23 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "description": "Returns release metadata for the provided repository and semantic version. Hotfix\n versions (e.g. v8.5.2) map back to the base release tag (v8.5.0).\n .FUNCTIONALITY\n Entrypoint,AnyTenant\n .ROLE\n CIPP.Core.Read\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", - "x-cipp-role": "CIPP.Core.Read", + "description": "Lists service principals in a customer tenant owned by the partner or specified vendor tenant IDs, useful for identifying GDAP-related app registrations.", + "x-cipp-role": "Tenant.Relationship.Read", "parameters": [ { - "name": "Owner", + "name": "ownerType", "in": "query", "required": false, "schema": { @@ -26281,7 +31433,10 @@ } }, { - "name": "Repository", + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "vendorTenantIds", "in": "query", "required": false, "schema": { @@ -26291,11 +31446,11 @@ ] } }, - "/api/ListGlobalAddressList": { + "/api/ListGeneratedReports": { "get": { - "summary": "ListGlobalAddressList", + "summary": "Lists generated reports from the CIPP Report Builder, filterable by tenant or report GUID.", "tags": [ - "Email-Exchange > Reports" + "Tools" ], "security": [ { @@ -26314,31 +31469,79 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Exchange.Mailbox.Read", + "description": "Lists generated reports from the CIPP Report Builder, filterable by tenant or report GUID.", + "x-cipp-role": "CIPP.Core.Read", "parameters": [ + { + "name": "ReportGUID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/tenantFilter" } ] } }, - "/api/ListGraphBulkRequest": { - "post": { - "summary": "ListGraphBulkRequest", + "/api/ListGenericTestFunction": { + "get": { + "summary": "Returns the original request URL for debugging purposes.", "tags": [ - "CIPP > Core" + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Returns the original request URL for debugging purposes. Used for CIPP platform diagnostics.", + "x-cipp-role": "CIPP.Core.Read" + } + }, + "/api/ListGitHubReleaseNotes": { + "get": { + "summary": "Retrieves release notes for a GitHub repository.", + "tags": [ + "Tools > GitHub" ], "security": [ { @@ -26357,18 +31560,120 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Returns release metadata for the provided repository and semantic version. Hotfix\n versions (e.g. v8.5.2) map back to the base release tag (v8.5.0).", + "x-cipp-role": "CIPP.Core.Read", + "parameters": [ + { + "name": "Owner", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Repository", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/ListGlobalAddressList": { + "get": { + "summary": "Lists all recipients in the Exchange Online Global Address List, including display names, email addresses, and recipi...", + "tags": [ + "Email-Exchange > Reports" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Lists all recipients in the Exchange Online Global Address List, including display names, email addresses, and recipient types.", + "x-cipp-role": "Exchange.Mailbox.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListGraphBulkRequest": { + "post": { + "summary": "Executes multiple Microsoft Graph API requests in a single batch call for a given tenant.", + "tags": [ + "CIPP > Core" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Executes multiple Microsoft Graph API requests in a single batch call for a given tenant. Accepts an array of request objects in the body.", "x-cipp-role": "CIPP.Core.Read", "requestBody": { "required": true, @@ -26401,7 +31706,7 @@ }, "/api/ListGraphExplorerPresets": { "get": { - "summary": "ListGraphExplorerPresets", + "summary": "Lists saved Graph Explorer query presets for the current user or shared presets.", "tags": [ "Uncategorized" ], @@ -26422,18 +31727,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists saved Graph Explorer query presets for the current user or shared presets.", "x-cipp-role": "CIPP.Core.Read", "parameters": [ { @@ -26447,9 +31753,77 @@ ] } }, + "/api/ListGraphReports": { + "get": { + "summary": "Retrieves Microsoft 365 usage reports (Graph or Office reports) for a tenant, such as email activity, OneDrive usage,...", + "tags": [ + "Tenant > Reports" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Retrieves Microsoft 365 usage reports (Graph or Office reports) for a tenant, such as email activity, OneDrive usage, or Teams device usage.", + "x-cipp-role": "Tenant.Reports.Read", + "parameters": [ + { + "name": "period", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "report", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "type", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, "/api/ListGraphRequest": { "get": { - "summary": "ListGraphRequest", + "summary": "Proxies an arbitrary Microsoft Graph API GET request for a tenant.", "tags": [ "CIPP > Core" ], @@ -26470,18 +31844,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Proxies an arbitrary Microsoft Graph API GET request for a tenant. Supports custom endpoints, filters, pagination, and field selection via query parameters.", "x-cipp-role": "CIPP.Core.Read", "parameters": [ { @@ -26632,21 +32007,36 @@ "schema": { "type": "object", "properties": { - "gdapRoles": { + "blockType": { "$ref": "#/components/schemas/LabelValue" }, - "id": { + "dbCacheType": { "$ref": "#/components/schemas/LabelValue" }, - "ignoreMissingRoles": { + "dbFormat": { + "$ref": "#/components/schemas/LabelValue" + }, + "includeRawAttachments": { "type": "boolean" }, "ooo": { "type": "object", "properties": { + "AutoDeclineFutureRequestsWhenOOF": { + "type": "boolean" + }, "AutoReplyState": { "$ref": "#/components/schemas/LabelValue" }, + "CreateOOFEvent": { + "type": "boolean" + }, + "DeclineEventsForScheduledOOF": { + "type": "boolean" + }, + "DeclineMeetingMessage": { + "type": "string" + }, "EndTime": { "type": "string", "format": "date-time" @@ -26657,12 +32047,21 @@ "InternalMessage": { "type": "string" }, + "OOFEventSubject": { + "type": "string" + }, "StartTime": { "type": "string", "format": "date-time" } } }, + "postExecution": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LabelValue" + } + }, "recipientLimits": { "type": "object", "properties": { @@ -26671,11 +32070,26 @@ } } }, - "remapRoles": { - "type": "string" + "recurrence": { + "$ref": "#/components/schemas/LabelValue" }, - "standardsExcludeAllTenants": { + "removeRemediation": { "type": "boolean" + }, + "scheduleName": { + "type": "string" + }, + "selectedTest": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LabelValue" + } + }, + "templateName": { + "type": "string" + }, + "testSuite": { + "$ref": "#/components/schemas/LabelValue" } } } @@ -26686,7 +32100,7 @@ }, "/api/ListGroupSenderAuthentication": { "get": { - "summary": "ListGroupSenderAuthentication", + "summary": "Lists sender authentication settings for an Exchange distribution group or mail-enabled security group, controlling w...", "tags": [ "Identity > Administration > Groups" ], @@ -26707,18 +32121,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists sender authentication settings for an Exchange distribution group or mail-enabled security group, controlling who can send to the group.", "x-cipp-role": "Exchange.Groups.Read", "parameters": [ { @@ -26766,13 +32181,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -26793,7 +32208,7 @@ }, "/api/ListGroups": { "get": { - "summary": "ListGroups", + "summary": "Lists Entra ID groups for a tenant, including group members and owners.", "tags": [ "Identity > Administration > Groups" ], @@ -26814,18 +32229,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists Entra ID groups for a tenant, including group members and owners. Supports UseReportDB=true query parameter to retrieve cached data from the reporting database for significantly better performance, especially when querying AllTenants.", "x-cipp-role": "Identity.Group.Read", "parameters": [ { @@ -26870,15 +32286,23 @@ }, { "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "UseReportDB", + "in": "query", + "required": false, + "schema": { + "type": "string" + } } ] } }, - "/api/ListHaloClients": { + "/api/ListHVEAccounts": { "get": { - "summary": "ListHaloClients", + "summary": "Lists High Volume Email (HVE) accounts and billing policies for a tenant.", "tags": [ - "Uncategorized" + "Email-Exchange > Administration" ], "security": [ { @@ -26897,65 +32321,56 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "CIPP.Extension.Read", - "x-cipp-dynamic-options": true - } - }, - "/api/ListIPWhitelist": { - "get": { - "summary": "ListIPWhitelist", - "tags": [ - "Uncategorized" - ], - "security": [ + "description": "Lists High Volume Email (HVE) accounts and billing policies for a tenant. Supports UseReportDB=true query parameter to retrieve cached data from the reporting database for significantly better performance, especially when querying AllTenants.", + "x-cipp-role": "Exchange.Mailbox.Read", + "parameters": [ { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StandardResults" - } - } + "name": "Identity", + "in": "query", + "required": false, + "schema": { + "type": "string" } }, - "400": { - "description": "Bad request — missing required field or invalid input" - }, - "401": { - "description": "Unauthorized — invalid or missing bearer token" + { + "name": "ListBillingPolicies", + "in": "query", + "required": false, + "schema": { + "type": "string" + } }, - "403": { - "description": "Forbidden — caller lacks the required RBAC role" + { + "$ref": "#/components/parameters/tenantFilter" }, - "500": { - "description": "Internal server error" + { + "name": "UseReportDB", + "in": "query", + "required": false, + "schema": { + "type": "string" + } } - }, - "x-cipp-role": "CIPP.Core.Read" + ] } }, - "/api/ListInactiveAccounts": { + "/api/ListHaloClients": { "get": { - "summary": "ListInactiveAccounts", + "summary": "Lists client records from the configured HaloPSA instance for tenant mapping purposes.", "tags": [ - "Identity > Reports" + "Uncategorized" ], "security": [ { @@ -26974,18 +32389,98 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists client records from the configured HaloPSA instance for tenant mapping purposes.", + "x-cipp-role": "CIPP.Extension.Read", + "x-cipp-dynamic-options": true + } + }, + "/api/ListIPWhitelist": { + "get": { + "summary": "Lists trusted IP addresses configured in CIPP for IP-based access control.", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Lists trusted IP addresses configured in CIPP for IP-based access control.", + "x-cipp-role": "CIPP.Core.Read" + } + }, + "/api/ListInactiveAccounts": { + "get": { + "summary": "Lists user accounts that have not signed in for a configurable number of days (default 180), based on sign-in activit...", + "tags": [ + "Identity > Reports" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Lists user accounts that have not signed in for a configurable number of days (default 180), based on sign-in activity data.", "x-cipp-role": "Tenant.Directory.Read", "parameters": [ { @@ -27004,7 +32499,7 @@ }, "/api/ListIntuneIntents": { "get": { - "summary": "ListIntuneIntents", + "summary": "Lists Intune security baseline and endpoint protection intents (legacy template-based policies) with their settings a...", "tags": [ "Uncategorized" ], @@ -27025,18 +32520,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists Intune security baseline and endpoint protection intents (legacy template-based policies) with their settings and categories.", "x-cipp-role": "Endpoint.MEM.Read", "parameters": [ { @@ -27047,7 +32543,7 @@ }, "/api/ListIntunePolicy": { "get": { - "summary": "ListIntunePolicy", + "summary": "Lists Intune device configuration policies for a tenant.", "tags": [ "Endpoint > MEM" ], @@ -27068,20 +32564,29 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists Intune device configuration policies for a tenant. Supports UseReportDB=true query parameter to retrieve cached data from the reporting database for significantly better performance, especially when querying AllTenants.", "x-cipp-role": "Endpoint.MEM.Read", "parameters": [ + { + "name": "DefinitionIds", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, { "name": "ID", "in": "query", @@ -27090,6 +32595,14 @@ "type": "string" } }, + { + "name": "IncludeSettingDefinitions", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/tenantFilter" }, @@ -27100,6 +32613,14 @@ "schema": { "type": "string" } + }, + { + "name": "UseReportDB", + "in": "query", + "required": false, + "schema": { + "type": "string" + } } ] } @@ -27127,13 +32648,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -27154,7 +32675,7 @@ }, "/api/ListIntuneReusableSettings": { "get": { - "summary": "ListIntuneReusableSettings", + "summary": "Lists Intune reusable policy settings for a tenant.", "tags": [ "Endpoint > MEM" ], @@ -27175,18 +32696,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists Intune reusable policy settings for a tenant. Supports UseReportDB=true query parameter to retrieve cached data from the reporting database for significantly better performance, especially when querying AllTenants.", "x-cipp-role": "Endpoint.MEM.Read", "parameters": [ { @@ -27199,13 +32721,21 @@ }, { "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "UseReportDB", + "in": "query", + "required": false, + "schema": { + "type": "string" + } } ] } }, "/api/ListIntuneScript": { "get": { - "summary": "ListIntuneScript", + "summary": "Lists Intune device management scripts (Windows, macOS, Linux, and remediation scripts) for a tenant.", "tags": [ "Endpoint > MEM" ], @@ -27226,22 +32756,31 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists Intune device management scripts (Windows, macOS, Linux, and remediation scripts) for a tenant. Supports UseReportDB=true query parameter to retrieve cached data from the reporting database for significantly better performance, especially when querying AllTenants.", "x-cipp-role": "Endpoint.MEM.Read", "parameters": [ { "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "UseReportDB", + "in": "query", + "required": false, + "schema": { + "type": "string" + } } ] } @@ -27269,13 +32808,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -27312,7 +32851,7 @@ }, "/api/ListJITAdmin": { "get": { - "summary": "ListJITAdmin", + "summary": "List Just-in-time admin users for a tenant or all tenants.", "tags": [ "Identity > Administration > Users" ], @@ -27333,19 +32872,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "description": "List Just-in-time admin users for a tenant or all tenants.\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", + "description": "List Just-in-time admin users for a tenant or all tenants.", "x-cipp-role": "Identity.Role.Read", "parameters": [ { @@ -27356,7 +32895,7 @@ }, "/api/ListJITAdminTemplates": { "post": { - "summary": "ListJITAdminTemplates", + "summary": "Lists Just-in-Time admin role templates that define temporary admin role assignments.", "tags": [ "Identity > Administration > Users" ], @@ -27377,18 +32916,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists Just-in-Time admin role templates that define temporary admin role assignments.", "x-cipp-role": "Identity.Role.Read", "parameters": [ { @@ -27430,7 +32970,7 @@ }, "/api/ListKnownIPDb": { "get": { - "summary": "ListKnownIPDb", + "summary": "Lists known IP address entries from the CIPP IP database, optionally filtered by tenant.", "tags": [ "Uncategorized" ], @@ -27451,18 +32991,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists known IP address entries from the CIPP IP database, optionally filtered by tenant.", "x-cipp-role": "CIPP.Core.Read", "parameters": [ { @@ -27473,7 +33014,51 @@ }, "/api/ListLicenses": { "get": { - "summary": "ListLicenses", + "summary": "Lists Microsoft 365 license SKUs and their assigned/available counts for a tenant.", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Lists Microsoft 365 license SKUs and their assigned/available counts for a tenant. For AllTenants queries, consider using ListDBCache for better performance.", + "x-cipp-role": "Tenant.Directory.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListLicensesReport": { + "get": { + "summary": "Lists a detailed license overview across all tenants or a single tenant, including SKU breakdowns, costs, and availab...", "tags": [ "Tenant > Reports" ], @@ -27494,20 +33079,29 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists a detailed license overview across all tenants or a single tenant, including SKU breakdowns, costs, and availability.", "x-cipp-role": "Tenant.Directory.Read", "parameters": [ + { + "name": "QueueId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/tenantFilter" } @@ -27516,7 +33110,7 @@ }, "/api/ListLogs": { "get": { - "summary": "ListLogs", + "summary": "Lists CIPP platform audit logs with filtering by severity, date range, tenant, and user.", "tags": [ "Uncategorized" ], @@ -27537,18 +33131,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists CIPP platform audit logs with filtering by severity, date range, tenant, and user. Supports listing available log categories.", "x-cipp-role": "CIPP.Core.Read", "parameters": [ { @@ -27650,9 +33245,61 @@ ] } }, + "/api/ListMDEOnboarding": { + "get": { + "summary": "Lists Microsoft Defender for Endpoint onboarding status for devices in a tenant.", + "tags": [ + "Security" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Lists Microsoft Defender for Endpoint onboarding status for devices in a tenant. Supports UseReportDB=true query parameter to retrieve cached data from the reporting database for significantly better performance. Automatically uses the reporting database when querying AllTenants.", + "x-cipp-role": "Security.Defender.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "UseReportDB", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, "/api/ListMFAUsers": { "get": { - "summary": "ListMFAUsers", + "summary": "Lists users and their MFA registration status for a tenant.", "tags": [ "Identity > Reports" ], @@ -27673,18 +33320,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists users and their MFA registration status for a tenant. Supports UseReportDB=true query parameter to retrieve cached data from the reporting database for significantly better performance, especially when querying AllTenants.", "x-cipp-role": "Identity.User.Read", "parameters": [ { @@ -27703,7 +33351,7 @@ }, "/api/ListMailQuarantine": { "get": { - "summary": "ListMailQuarantine", + "summary": "Lists quarantined email messages in Exchange Online Protection for a tenant.", "tags": [ "Email-Exchange > Spamfilter" ], @@ -27724,18 +33372,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists quarantined email messages in Exchange Online Protection for a tenant.", "x-cipp-role": "Exchange.SpamFilter.Read", "parameters": [ { @@ -27746,7 +33395,7 @@ }, "/api/ListMailQuarantineMessage": { "get": { - "summary": "ListMailQuarantineMessage", + "summary": "Exports and retrieves the raw EML content of a specific quarantined email message by its Identity.", "tags": [ "Email-Exchange > Spamfilter" ], @@ -27767,18 +33416,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Exports and retrieves the raw EML content of a specific quarantined email message by its Identity.", "x-cipp-role": "Exchange.SpamFilter.Read", "parameters": [ { @@ -27797,7 +33447,7 @@ }, "/api/ListMailboxCAS": { "get": { - "summary": "ListMailboxCAS", + "summary": "Lists Client Access Settings (CAS) for Exchange Online mailboxes, showing which protocols are enabled (OWA, IMAP, POP...", "tags": [ "Email-Exchange > Reports" ], @@ -27818,18 +33468,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists Client Access Settings (CAS) for Exchange Online mailboxes, showing which protocols are enabled (OWA, IMAP, POP, MAPI, EWS, ActiveSync).", "x-cipp-role": "Exchange.Mailbox.Read", "parameters": [ { @@ -27840,7 +33491,7 @@ }, "/api/ListMailboxForwarding": { "get": { - "summary": "ListMailboxForwarding", + "summary": "Lists mailbox forwarding rules and configurations for a tenant.", "tags": [ "Email-Exchange > Reports" ], @@ -27861,18 +33512,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists mailbox forwarding rules and configurations for a tenant. Supports UseReportDB=true query parameter to retrieve cached data from the reporting database for significantly better performance, especially when querying AllTenants.", "x-cipp-role": "Exchange.Mailbox.Read", "parameters": [ { @@ -27891,7 +33543,7 @@ }, "/api/ListMailboxMobileDevices": { "get": { - "summary": "ListMailboxMobileDevices", + "summary": "Lists mobile devices associated with a specific Exchange Online mailbox.", "tags": [ "Email-Exchange > Administration" ], @@ -27912,18 +33564,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists mobile devices associated with a specific Exchange Online mailbox.", "x-cipp-role": "Exchange.Mailbox.Read", "parameters": [ { @@ -27942,7 +33595,7 @@ }, "/api/ListMailboxRestores": { "get": { - "summary": "ListMailboxRestores", + "summary": "Lists mailbox restore requests and their status for Exchange Online, with optional statistics for a specific restore...", "tags": [ "Email-Exchange > Tools" ], @@ -27963,18 +33616,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists mailbox restore requests and their status for Exchange Online, with optional statistics for a specific restore operation.", "x-cipp-role": "Exchange.Mailbox.Read", "parameters": [ { @@ -28009,7 +33663,7 @@ }, "/api/ListMailboxRules": { "get": { - "summary": "ListMailboxRules", + "summary": "Lists inbox rules configured on mailboxes in a tenant.", "tags": [ "Email-Exchange > Administration" ], @@ -28030,18 +33684,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists inbox rules configured on mailboxes in a tenant. Supports UseReportDB=true query parameter to retrieve cached data from the reporting database for significantly better performance, especially when querying AllTenants.", "x-cipp-role": "Exchange.Mailbox.Read", "parameters": [ { @@ -28060,7 +33715,7 @@ }, "/api/ListMailboxes": { "get": { - "summary": "ListMailboxes", + "summary": "Lists Exchange Online mailboxes for a tenant.", "tags": [ "Email-Exchange > Administration" ], @@ -28081,18 +33736,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists Exchange Online mailboxes for a tenant. Supports UseReportDB=true query parameter to retrieve cached data from the reporting database for significantly better performance, especially when querying AllTenants.", "x-cipp-role": "Exchange.Mailbox.Read", "parameters": [ { @@ -28119,7 +33775,7 @@ }, "/api/ListMalwareFilters": { "get": { - "summary": "ListMalwareFilters", + "summary": "Lists malware filter policies and their associated rules configured in Exchange Online Protection.", "tags": [ "Email-Exchange > Reports" ], @@ -28140,18 +33796,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists malware filter policies and their associated rules configured in Exchange Online Protection.", "x-cipp-role": "Exchange.SpamFilter.Read", "parameters": [ { @@ -28162,7 +33819,7 @@ }, "/api/ListMessageTrace": { "post": { - "summary": "ListMessageTrace", + "summary": "Traces email message delivery in Exchange Online, searchable by message ID, sender, recipient, and date range.", "tags": [ "Email-Exchange > Tools" ], @@ -28183,18 +33840,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Traces email message delivery in Exchange Online, searchable by message ID, sender, recipient, and date range.", "x-cipp-role": "Exchange.TransportRule.Read", "requestBody": { "required": true, @@ -28208,54 +33866,67 @@ "enum": [ "relative", "startEnd" - ] + ], + "description": "dateFilter parameter" }, "days": { - "type": "number" + "type": "number", + "description": "days parameter" }, "endDate": { "type": "string", - "format": "date-time" + "format": "date-time", + "description": "endDate parameter" }, "fromIP": { - "type": "string" + "type": "string", + "description": "fromIP parameter" }, "ID": { - "type": "string" + "type": "string", + "description": "ID parameter" }, "recipient": { "type": "array", "items": { "type": "string" - } + }, + "description": "recipient parameter" }, "sender": { "type": "array", "items": { "type": "string" - } + }, + "description": "sender parameter" }, "startDate": { "type": "string", - "format": "date-time" + "format": "date-time", + "description": "startDate parameter" }, "status": { "type": "array", "items": { "type": "string" - } + }, + "description": "status parameter" }, "tenantFilter": { - "type": "string" + "type": "string", + "description": "tenantFilter parameter" }, "toIP": { - "type": "string" + "type": "string", + "description": "toIP parameter" }, "traceDetail": { - "type": "string" + "type": "string", + "description": "traceDetail parameter" }, "MessageId": { - "type": "string" + "type": "string", + "description": "Body parameter from PS1: MessageId" } }, "required": [ @@ -28269,7 +33940,7 @@ }, "/api/ListNamedLocations": { "get": { - "summary": "ListNamedLocations", + "summary": "Lists Conditional Access named locations (IP ranges and country-based locations) configured in a tenant.", "tags": [ "Uncategorized" ], @@ -28290,18 +33961,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists Conditional Access named locations (IP ranges and country-based locations) configured in a tenant.", "x-cipp-role": "Tenant.ConditionalAccess.Read", "parameters": [ { @@ -28312,7 +33984,7 @@ }, "/api/ListNewUserDefaults": { "post": { - "summary": "ListNewUserDefaults", + "summary": "Lists default templates for new user creation, including default domain, usage location, and license assignments.", "tags": [ "Identity > Administration > Users" ], @@ -28333,18 +34005,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists default templates for new user creation, including default domain, usage location, and license assignments.", "x-cipp-role": "Identity.User.Read", "parameters": [ { @@ -28386,7 +34059,7 @@ }, "/api/ListNotificationConfig": { "get": { - "summary": "ListNotificationConfig", + "summary": "Retrieves the CIPP notification configuration, including alert channels, severity filters, and notification targets.", "tags": [ "Uncategorized" ], @@ -28407,25 +34080,26 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Retrieves the CIPP notification configuration, including alert channels, severity filters, and notification targets.", "x-cipp-role": "CIPP.AppSettings.Read", "x-cipp-dynamic-options": true } }, "/api/ListOAuthApps": { "get": { - "summary": "ListOAuthApps", + "summary": "Lists OAuth application consent grants and permissions for a tenant.", "tags": [ "Tenant > Reports" ], @@ -28446,29 +34120,77 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists OAuth application consent grants and permissions for a tenant. Supports UseReportDB=true query parameter to retrieve cached data from the reporting database for significantly better performance, especially when querying AllTenants.", "x-cipp-role": "Tenant.Application.Read", "parameters": [ { "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "UseReportDB", + "in": "query", + "required": false, + "schema": { + "type": "string" + } } ] } }, + "/api/ListOffboardTenants": { + "get": { + "summary": "Lists tenants available for offboarding, including tenants in error state.", + "tags": [ + "Tenant > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Lists tenants available for offboarding, including tenants in error state.", + "x-cipp-role": "Tenant.Administration.ReadWrite" + } + }, "/api/ListOoO": { "get": { - "summary": "ListOoO", + "summary": "Retrieves Out of Office (automatic reply) settings for a specific Exchange Online user.", "tags": [ "Email-Exchange > Administration" ], @@ -28489,18 +34211,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Retrieves Out of Office (automatic reply) settings for a specific Exchange Online user.", "x-cipp-role": "Exchange.Mailbox.Read", "parameters": [ { @@ -28519,7 +34242,7 @@ }, "/api/ListOrg": { "get": { - "summary": "ListOrg", + "summary": "Retrieves the Entra ID organization profile for a tenant, including display name, addresses, and tenant metadata.", "tags": [ "Uncategorized" ], @@ -28540,18 +34263,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Retrieves the Entra ID organization profile for a tenant, including display name, addresses, and tenant metadata.", "x-cipp-role": "CIPP.Core.Read", "parameters": [ { @@ -28562,7 +34286,7 @@ }, "/api/ListPartnerRelationships": { "get": { - "summary": "ListPartnerRelationships", + "summary": "Lists cross-tenant access policy partner configurations for a tenant, showing delegated admin and collaboration setti...", "tags": [ "Uncategorized" ], @@ -28583,18 +34307,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists cross-tenant access policy partner configurations for a tenant, showing delegated admin and collaboration settings.", "x-cipp-role": "Tenant.Relationship.Read", "parameters": [ { @@ -28605,7 +34330,7 @@ }, "/api/ListPendingWebhooks": { "get": { - "summary": "ListPendingWebhooks", + "summary": "Lists pending incoming webhook events that have not yet been processed by CIPP.", "tags": [ "Uncategorized" ], @@ -28626,24 +34351,25 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists pending incoming webhook events that have not yet been processed by CIPP.", "x-cipp-role": "CIPP.Alert.Read" } }, "/api/ListPerUserMFA": { "get": { - "summary": "ListPerUserMFA", + "summary": "Lists per-user MFA state (enabled, enforced, disabled) for users in a tenant via the legacy MFA management API.", "tags": [ "Identity > Administration > Users" ], @@ -28664,18 +34390,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists per-user MFA state (enabled, enforced, disabled) for users in a tenant via the legacy MFA management API.", "x-cipp-role": "Identity.User.Read", "parameters": [ { @@ -28702,7 +34429,7 @@ }, "/api/ListPotentialApps": { "post": { - "summary": "ListPotentialApps", + "summary": "Searches application repositories (WinGet, Chocolatey) for available applications matching a search string.", "tags": [ "Uncategorized" ], @@ -28723,18 +34450,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Searches application repositories (WinGet, Chocolatey) for available applications matching a search string.", "x-cipp-role": "Endpoint.Application.Read", "requestBody": { "required": true, @@ -28786,6 +34514,9 @@ "detectionPath": { "type": "string" }, + "detectionScript": { + "type": "string" + }, "DisableRestart": { "type": "boolean" }, @@ -28801,6 +34532,9 @@ "$ref": "#/components/schemas/LabelValue" } }, + "excludeGroup": { + "type": "string" + }, "InstallAsSystem": { "type": "boolean" }, @@ -28854,6 +34588,12 @@ "SharedComputerActivation": { "type": "boolean" }, + "templateDescription": { + "type": "string" + }, + "templateName": { + "type": "string" + }, "type": { "type": "string" }, @@ -28865,6 +34605,9 @@ }, "useCustomXml": { "type": "boolean" + }, + "useDetectionScript": { + "type": "boolean" } } } @@ -28875,7 +34618,7 @@ }, "/api/ListQuarantinePolicy": { "post": { - "summary": "ListQuarantinePolicy", + "summary": "Lists quarantine policies configured in Exchange Online Protection, controlling end-user access to quarantined messages.", "tags": [ "Email-Exchange > Spamfilter" ], @@ -28896,18 +34639,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists quarantine policies configured in Exchange Online Protection, controlling end-user access to quarantined messages.", "x-cipp-role": "Exchange.SpamFilter.Read", "parameters": [ { @@ -28942,9 +34686,87 @@ } } }, + "/api/ListReportBuilderTemplates": { + "get": { + "summary": "Lists saved Report Builder templates that define custom report configurations with data blocks and formatting.", + "tags": [ + "Tools" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Lists saved Report Builder templates that define custom report configurations with data blocks and formatting.", + "x-cipp-role": "CIPP.Core.Read" + } + }, + "/api/ListResellerRelationshipLink": { + "get": { + "summary": "Retrieves the indirect reseller relationship invitation link from Partner Center for onboarding new customer tenants.", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Retrieves the indirect reseller relationship invitation link from Partner Center for onboarding new customer tenants.", + "x-cipp-role": "Tenant.Relationship.Read" + } + }, "/api/ListRestrictedUsers": { "get": { - "summary": "ListRestrictedUsers", + "summary": "Lists users from the restricted senders list in Exchange Online.", "tags": [ "Email-Exchange > Administration" ], @@ -28965,19 +34787,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "description": "Lists users from the restricted senders list in Exchange Online.\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)\n # Interact with query parameters or the body of the request.\n $TenantFilter = $Request.Query.tenantFilter", + "description": "Lists users from the restricted senders list in Exchange Online.", "x-cipp-role": "Exchange.Mailbox.Read", "parameters": [ { @@ -28986,6 +34808,100 @@ ] } }, + "/api/ListRetentionCompliancePolicy": { + "get": { + "summary": "Lists retention compliance policies and their associated rules from the Security & Compliance Center.", + "tags": [ + "Security > Compliance-Retention" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Lists retention compliance policies and their associated rules from the Security & Compliance Center.", + "x-cipp-role": "Security.RetentionCompliancePolicy.Read", + "x-cipp-dynamic-options": true, + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListRetentionCompliancePolicyTemplates": { + "get": { + "summary": "Lists saved retention compliance policy templates for deploying standardized retention configurations.", + "tags": [ + "Security > Compliance-Retention" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Lists saved retention compliance policy templates for deploying standardized retention configurations.", + "x-cipp-role": "Security.RetentionCompliancePolicy.Read", + "parameters": [ + { + "name": "ID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, "/api/ListRoles": { "get": { "summary": "ListRoles", @@ -29009,13 +34925,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -29031,7 +34947,7 @@ }, "/api/ListRoomLists": { "get": { - "summary": "ListRoomLists", + "summary": "Lists room list distribution groups and their members in Exchange Online.", "tags": [ "Email-Exchange > Resources" ], @@ -29052,18 +34968,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists room list distribution groups and their members in Exchange Online.", "x-cipp-role": "Exchange.Room.Read", "parameters": [ { @@ -29098,7 +35015,7 @@ }, "/api/ListRooms": { "get": { - "summary": "ListRooms", + "summary": "Lists room mailboxes (meeting rooms) in Exchange Online for a tenant, with optional detailed view by room ID.", "tags": [ "Email-Exchange > Resources" ], @@ -29119,18 +35036,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists room mailboxes (meeting rooms) in Exchange Online for a tenant, with optional detailed view by room ID.", "x-cipp-role": "Exchange.Room.Read", "parameters": [ { @@ -29149,7 +35067,7 @@ }, "/api/ListSafeAttachmentsFilters": { "get": { - "summary": "ListSafeAttachmentsFilters", + "summary": "Lists Safe Attachments policies and their associated rules configured in Microsoft Defender for Office 365.", "tags": [ "Email-Exchange > Reports" ], @@ -29170,18 +35088,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists Safe Attachments policies and their associated rules configured in Microsoft Defender for Office 365.", "x-cipp-role": "Exchange.SpamFilter.Read", "parameters": [ { @@ -29192,7 +35111,7 @@ }, "/api/ListSafeLinksPolicy": { "get": { - "summary": "ListSafeLinksPolicy", + "summary": "This function is used to list the Safe Links policies in the tenant, including unmatched rules and policies.", "tags": [ "Security > Safe-Links-Policy" ], @@ -29213,19 +35132,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "description": "This function is used to list the Safe Links policies in the tenant, including unmatched rules and policies.\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)\n $APIName = $Request.Params.CIPPEndpoint\n $Headers = $Request.Headers", + "description": "This function is used to list the Safe Links policies in the tenant, including unmatched rules and policies.", "x-cipp-role": "Security.SafeLinksPolicy.Read", "parameters": [ { @@ -29236,7 +35155,7 @@ }, "/api/ListSafeLinksPolicyDetails": { "post": { - "summary": "ListSafeLinksPolicyDetails", + "summary": "This function retrieves details for a specific Safe Links policy and rule.", "tags": [ "Security > Safe-Links-Policy" ], @@ -29257,19 +35176,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "description": "This function retrieves details for a specific Safe Links policy and rule.\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", + "description": "This function retrieves details for a specific Safe Links policy and rule.", "x-cipp-role": "Exchange.SpamFilter.Read", "parameters": [ { @@ -29320,7 +35239,7 @@ }, "/api/ListSafeLinksPolicyTemplateDetails": { "post": { - "summary": "ListSafeLinksPolicyTemplateDetails", + "summary": "This function retrieves details for a specific Safe Links policy template.", "tags": [ "Security > Safe-Links-Policy" ], @@ -29341,19 +35260,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "description": "This function retrieves details for a specific Safe Links policy template.\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", + "description": "This function retrieves details for a specific Safe Links policy template.", "x-cipp-role": "Exchange.SafeLinks.Read", "parameters": [ { @@ -29384,7 +35303,7 @@ }, "/api/ListSafeLinksPolicyTemplates": { "get": { - "summary": "ListSafeLinksPolicyTemplates", + "summary": "Lists saved Safe Links policy templates for deploying standardized Safe Links configurations.", "tags": [ "Security > Safe-Links-Policy" ], @@ -29405,18 +35324,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists saved Safe Links policy templates for deploying standardized Safe Links configurations.", "x-cipp-role": "Exchange.SafeLinks.Read", "parameters": [ { @@ -29432,7 +35352,7 @@ }, "/api/ListScheduledItemDetails": { "post": { - "summary": "ListScheduledItemDetails", + "summary": "Retrieves detailed information about a specific scheduled task by its RowKey, including execution results and task pa...", "tags": [ "CIPP > Scheduler" ], @@ -29453,18 +35373,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Retrieves detailed information about a specific scheduled task by its RowKey, including execution results and task parameters.", "x-cipp-role": "CIPP.Scheduler.Read", "parameters": [ { @@ -29495,7 +35416,7 @@ }, "/api/ListScheduledItems": { "post": { - "summary": "ListScheduledItems", + "summary": "Lists scheduled tasks in CIPP, filterable by tenant or task ID.", "tags": [ "CIPP > Scheduler" ], @@ -29516,18 +35437,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists scheduled tasks in CIPP, filterable by tenant or task ID. Returns task name, command, schedule, and last execution status.", "x-cipp-role": "CIPP.Scheduler.Read", "parameters": [ { @@ -29609,11 +35531,11 @@ } } }, - "/api/ListServiceHealth": { + "/api/ListSensitiveInfoType": { "get": { - "summary": "ListServiceHealth", + "summary": "Lists sensitive information types (SITs) configured in the Security & Compliance Center, optionally including built-i...", "tags": [ - "Tenant > Reports" + "Security > Compliance-SIT" ], "security": [ { @@ -29632,30 +35554,24 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Tenant.Administration.Read", + "description": "Lists sensitive information types (SITs) configured in the Security & Compliance Center, optionally including built-in types.", + "x-cipp-role": "Security.SensitiveInfoType.Read", + "x-cipp-dynamic-options": true, "parameters": [ { - "name": "defaultDomainName", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "displayName", + "name": "IncludeBuiltIn", "in": "query", "required": false, "schema": { @@ -29668,11 +35584,11 @@ ] } }, - "/api/ListSharedMailboxAccountEnabled": { + "/api/ListSensitiveInfoTypeTemplates": { "get": { - "summary": "ListSharedMailboxAccountEnabled", + "summary": "Lists saved sensitive information type templates for deploying custom SIT configurations.", "tags": [ - "Email-Exchange > Reports" + "Security > Compliance-SIT" ], "security": [ { @@ -29691,31 +35607,37 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Exchange.Mailbox.Read", + "description": "Lists saved sensitive information type templates for deploying custom SIT configurations.", + "x-cipp-role": "Security.SensitiveInfoType.Read", "parameters": [ { - "$ref": "#/components/parameters/tenantFilter" + "name": "ID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } } ] } }, - "/api/ListSharedMailboxStatistics": { + "/api/ListSensitivityLabel": { "get": { - "summary": "ListSharedMailboxStatistics", + "summary": "Lists sensitivity labels and label policies configured in the Security & Compliance Center for a tenant.", "tags": [ - "Email-Exchange > Administration" + "Security > Compliance-SensitivityLabel" ], "security": [ { @@ -29734,19 +35656,21 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Exchange.Mailbox.Read", + "description": "Lists sensitivity labels and label policies configured in the Security & Compliance Center for a tenant.", + "x-cipp-role": "Security.SensitivityLabel.Read", + "x-cipp-dynamic-options": true, "parameters": [ { "$ref": "#/components/parameters/tenantFilter" @@ -29754,11 +35678,11 @@ ] } }, - "/api/ListSharepointAdminUrl": { + "/api/ListSensitivityLabelTemplates": { "get": { - "summary": "ListSharepointAdminUrl", + "summary": "Lists saved sensitivity label templates for deploying standardized label configurations.", "tags": [ - "Teams-Sharepoint" + "Security > Compliance-SensitivityLabel" ], "security": [ { @@ -29777,39 +35701,37 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "CIPP.Core.Read", + "description": "Lists saved sensitivity label templates for deploying standardized label configurations.", + "x-cipp-role": "Security.SensitivityLabel.Read", "parameters": [ { - "name": "ReturnUrl", + "name": "ID", "in": "query", "required": false, "schema": { "type": "string" } - }, - { - "$ref": "#/components/parameters/tenantFilter" } ] } }, - "/api/ListSharepointQuota": { + "/api/ListServiceHealth": { "get": { - "summary": "ListSharepointQuota", + "summary": "Lists active Microsoft 365 service health issues and advisories for a tenant or all tenants.", "tags": [ - "Teams-Sharepoint" + "Tenant > Reports" ], "security": [ { @@ -29828,31 +35750,48 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Sharepoint.Admin.Read", + "description": "Lists active Microsoft 365 service health issues and advisories for a tenant or all tenants.", + "x-cipp-role": "Tenant.Administration.Read", "parameters": [ + { + "name": "defaultDomainName", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "displayName", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/tenantFilter" } ] } }, - "/api/ListSharepointSettings": { + "/api/ListSharedMailboxAccountEnabled": { "get": { - "summary": "ListSharepointSettings", + "summary": "Lists shared mailboxes that have direct sign-in enabled (account not disabled), which is a security concern.", "tags": [ - "Teams-Sharepoint" + "Email-Exchange > Reports" ], "security": [ { @@ -29871,19 +35810,20 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Sharepoint.Admin.Read", + "description": "Lists shared mailboxes that have direct sign-in enabled (account not disabled), which is a security concern.", + "x-cipp-role": "Exchange.Mailbox.Read", "parameters": [ { "$ref": "#/components/parameters/tenantFilter" @@ -29891,11 +35831,11 @@ ] } }, - "/api/ListSignIns": { + "/api/ListSharedMailboxStatistics": { "get": { - "summary": "ListSignIns", + "summary": "Lists shared mailboxes and their usage statistics for a tenant.", "tags": [ - "Identity > Reports" + "Email-Exchange > Administration" ], "security": [ { @@ -29914,61 +35854,30 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Identity.AuditLog.Read", + "description": "Lists shared mailboxes and their usage statistics for a tenant.", + "x-cipp-role": "Exchange.Mailbox.Read", "parameters": [ - { - "name": "Days", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "failedLogonsOnly", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "FailureThreshold", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "Filter", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, { "$ref": "#/components/parameters/tenantFilter" } ] } }, - "/api/ListSiteMembers": { + "/api/ListSharepointAdminUrl": { "get": { - "summary": "ListSiteMembers", + "summary": "Retrieves the SharePoint Admin Center URL for a tenant.", "tags": [ "Teams-Sharepoint" ], @@ -29989,22 +35898,23 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Sharepoint.Site.Read", + "description": "Retrieves the SharePoint Admin Center URL for a tenant.", + "x-cipp-role": "CIPP.Core.Read", "parameters": [ { - "name": "SiteId", + "name": "ReturnUrl", "in": "query", "required": false, "schema": { @@ -30017,9 +35927,9 @@ ] } }, - "/api/ListSites": { + "/api/ListSharepointQuota": { "get": { - "summary": "ListSites", + "summary": "Retrieves SharePoint Online storage quota usage for a tenant, showing used and total storage.", "tags": [ "Teams-Sharepoint" ], @@ -30040,55 +35950,32 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Sharepoint.Site.Read", + "description": "Retrieves SharePoint Online storage quota usage for a tenant, showing used and total storage.", + "x-cipp-role": "Sharepoint.Admin.Read", "parameters": [ { "$ref": "#/components/parameters/tenantFilter" - }, - { - "name": "Type", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "URLOnly", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "UserUPN", - "in": "query", - "required": false, - "schema": { - "type": "string" - } } ] } }, - "/api/ListSpamFilterTemplates": { + "/api/ListSharepointSettings": { "get": { - "summary": "ListSpamFilterTemplates", + "summary": "Retrieves SharePoint Online tenant-level settings and configuration.", "tags": [ - "Email-Exchange > Spamfilter" + "Teams-Sharepoint" ], "security": [ { @@ -30107,36 +35994,32 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Exchange.SpamFilter.Read", + "description": "Retrieves SharePoint Online tenant-level settings and configuration.", + "x-cipp-role": "Sharepoint.Admin.Read", "parameters": [ { - "name": "id", - "in": "query", - "required": false, - "schema": { - "type": "string" - } + "$ref": "#/components/parameters/tenantFilter" } ] } }, - "/api/ListSpamfilter": { + "/api/ListSignIns": { "get": { - "summary": "ListSpamfilter", + "summary": "Lists recent sign-in log entries for a tenant, filterable by various criteria.", "tags": [ - "Email-Exchange > Spamfilter" + "Identity > Reports" ], "security": [ { @@ -30155,66 +36038,47 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Exchange.SpamFilter.Read", - "x-cipp-dynamic-options": true, + "description": "Lists recent sign-in log entries for a tenant, filterable by various criteria. Supports AllTenants queries.", + "x-cipp-role": "Identity.AuditLog.Read", "parameters": [ { - "$ref": "#/components/parameters/tenantFilter" - } - ] - } - }, - "/api/ListStandards": { - "get": { - "summary": "ListStandards", - "tags": [ - "Tenant > Standards" - ], - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StandardResults" - } - } + "name": "Days", + "in": "query", + "required": false, + "schema": { + "type": "string" } }, - "400": { - "description": "Bad request — missing required field or invalid input" - }, - "401": { - "description": "Unauthorized — invalid or missing bearer token" + { + "name": "failedLogonsOnly", + "in": "query", + "required": false, + "schema": { + "type": "string" + } }, - "403": { - "description": "Forbidden — caller lacks the required RBAC role" + { + "name": "FailureThreshold", + "in": "query", + "required": false, + "schema": { + "type": "string" + } }, - "500": { - "description": "Internal server error" - } - }, - "x-cipp-role": "Tenant.Standards.Read", - "parameters": [ { - "name": "ShowConsolidated", + "name": "Filter", "in": "query", "required": false, "schema": { @@ -30227,11 +36091,11 @@ ] } }, - "/api/ListStandardsCompare": { + "/api/ListSiteMembers": { "get": { - "summary": "ListStandardsCompare", + "summary": "Lists members of a specific SharePoint site by site ID, including user display names and email addresses.", "tags": [ - "Tenant > Standards" + "Teams-Sharepoint" ], "security": [ { @@ -30250,22 +36114,23 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Tenant.BestPracticeAnalyser.Read", + "description": "Lists members of a specific SharePoint site by site ID, including user display names and email addresses.", + "x-cipp-role": "Sharepoint.Site.Read", "parameters": [ { - "name": "templateId", + "name": "SiteId", "in": "query", "required": false, "schema": { @@ -30278,9 +36143,9 @@ ] } }, - "/api/ListTeams": { + "/api/ListSites": { "get": { - "summary": "ListTeams", + "summary": "Lists SharePoint sites or OneDrive usage for a tenant.", "tags": [ "Teams-Sharepoint" ], @@ -30301,22 +36166,26 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Teams.Group.Read", + "description": "Lists SharePoint sites or OneDrive usage for a tenant. Requires a Type parameter (SharePointSiteUsage or OneDriveUsageAccount). Supports UseReportDB=true query parameter to retrieve cached data from the reporting database for significantly better performance, especially when querying AllTenants.", + "x-cipp-role": "Sharepoint.Site.Read", "parameters": [ { - "name": "ID", + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "Type", "in": "query", "required": false, "schema": { @@ -30324,10 +36193,15 @@ } }, { - "$ref": "#/components/parameters/tenantFilter" + "name": "URLOnly", + "in": "query", + "required": false, + "schema": { + "type": "string" + } }, { - "name": "type", + "name": "UseReportDB", "in": "query", "required": false, "schema": { @@ -30337,11 +36211,11 @@ ] } }, - "/api/ListTeamsActivity": { + "/api/ListSnoozedAlerts": { "get": { - "summary": "ListTeamsActivity", + "summary": "Lists alerts that have been snoozed (temporarily suppressed), filterable by cmdlet name.", "tags": [ - "Teams-Sharepoint" + "CIPP > Core" ], "security": [ { @@ -30360,25 +36234,23 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Teams.Activity.Read", + "description": "Lists alerts that have been snoozed (temporarily suppressed), filterable by cmdlet name. Returns snooze duration and scope details.", + "x-cipp-role": "CIPP.Alert.Read", "parameters": [ { - "$ref": "#/components/parameters/tenantFilter" - }, - { - "name": "Type", + "name": "CmdletName", "in": "query", "required": false, "schema": { @@ -30388,11 +36260,11 @@ ] } }, - "/api/ListTeamsLisLocation": { + "/api/ListSpamFilterTemplates": { "get": { - "summary": "ListTeamsLisLocation", + "summary": "Lists saved spam filter policy templates for Exchange Online Protection.", "tags": [ - "Teams-Sharepoint" + "Email-Exchange > Spamfilter" ], "security": [ { @@ -30411,31 +36283,37 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Teams.Voice.Read", + "description": "Lists saved spam filter policy templates for Exchange Online Protection.", + "x-cipp-role": "Exchange.SpamFilter.Read", "parameters": [ { - "$ref": "#/components/parameters/tenantFilter" + "name": "id", + "in": "query", + "required": false, + "schema": { + "type": "string" + } } ] } }, - "/api/ListTeamsVoice": { + "/api/ListSpamfilter": { "get": { - "summary": "ListTeamsVoice", + "summary": "Lists hosted content filter (anti-spam) policies and their rule states in Exchange Online Protection.", "tags": [ - "Teams-Sharepoint" + "Email-Exchange > Spamfilter" ], "security": [ { @@ -30454,19 +36332,21 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Teams.Voice.Read", + "description": "Lists hosted content filter (anti-spam) policies and their rule states in Exchange Online Protection.", + "x-cipp-role": "Exchange.SpamFilter.Read", + "x-cipp-dynamic-options": true, "parameters": [ { "$ref": "#/components/parameters/tenantFilter" @@ -30474,9 +36354,9 @@ ] } }, - "/api/ListTenantAlignment": { + "/api/ListStandards": { "get": { - "summary": "ListTenantAlignment", + "summary": "Lists configured tenant standards (compliance policies) and their settings, with optional consolidated view.", "tags": [ "Tenant > Standards" ], @@ -30497,69 +36377,40 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Tenant.Standards.Read" - } - }, - "/api/ListTenantAllowBlockList": { - "get": { - "summary": "ListTenantAllowBlockList", - "tags": [ - "Uncategorized" - ], - "security": [ + "description": "Lists configured tenant standards (compliance policies) and their settings, with optional consolidated view.", + "x-cipp-role": "Tenant.Standards.Read", + "parameters": [ { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StandardResults" - } - } + "name": "ShowConsolidated", + "in": "query", + "required": false, + "schema": { + "type": "string" } }, - "400": { - "description": "Bad request — missing required field or invalid input" - }, - "401": { - "description": "Unauthorized — invalid or missing bearer token" - }, - "403": { - "description": "Forbidden — caller lacks the required RBAC role" - }, - "500": { - "description": "Internal server error" - } - }, - "x-cipp-role": "Exchange.SpamFilter.Read", - "parameters": [ { "$ref": "#/components/parameters/tenantFilter" } ] } }, - "/api/ListTenantDetails": { + "/api/ListStandardsCompare": { "get": { - "summary": "ListTenantDetails", + "summary": "Compares current tenant configuration against applied standards, showing compliance status and drift for each standard.", "tags": [ - "Tenant > Administration > Tenant" + "Tenant > Standards" ], "security": [ { @@ -30578,29 +36429,38 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "CIPP.Core.Read", + "description": "Compares current tenant configuration against applied standards, showing compliance status and drift for each standard.", + "x-cipp-role": "Tenant.BestPracticeAnalyser.Read", "parameters": [ + { + "name": "templateId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/tenantFilter" } ] } }, - "/api/ListTenantDrift": { + "/api/ListStandardsCurrentState": { "get": { - "summary": "ListTenantDrift", + "summary": "Lists the current remediation state of standards applied to a specific tenant, including last check results.", "tags": [ "Tenant > Standards" ], @@ -30621,18 +36481,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists the current remediation state of standards applied to a specific tenant, including last check results.", "x-cipp-role": "Tenant.Standards.Read", "parameters": [ { @@ -30641,11 +36502,11 @@ ] } }, - "/api/ListTenantGroups": { - "post": { - "summary": "Entrypoint for listing tenant groups", + "/api/ListTeams": { + "get": { + "summary": "Lists Microsoft Teams teams for a tenant.", "tags": [ - "CIPP > Settings" + "Teams-Sharepoint" ], "security": [ { @@ -30664,22 +36525,23 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "CIPP.Core.Read", + "description": "Lists Microsoft Teams teams for a tenant. Supports UseReportDB=true query parameter to retrieve cached data from the reporting database for significantly better performance, especially when querying AllTenants.", + "x-cipp-role": "Teams.Group.Read", "parameters": [ { - "name": "groupId", + "name": "ID", "in": "query", "required": false, "schema": { @@ -30687,39 +36549,32 @@ } }, { - "name": "includeUsage", + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "type", "in": "query", "required": false, "schema": { "type": "string" } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "groupId": { - "type": "string" - }, - "includeUsage": { - "type": "string" - } - } - } + }, + { + "name": "UseReportDB", + "in": "query", + "required": false, + "schema": { + "type": "string" } } - } + ] } }, - "/api/ListTenantOnboarding": { + "/api/ListTeamsActivity": { "get": { - "summary": "ListTenantOnboarding", + "summary": "Lists Microsoft Teams user activity reports for a tenant.", "tags": [ - "Tenant > Administration" + "Teams-Sharepoint" ], "security": [ { @@ -30738,18 +36593,480 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists Microsoft Teams user activity reports for a tenant. Supports UseReportDB=true query parameter to retrieve cached data from the reporting database for significantly better performance, especially when querying AllTenants.", + "x-cipp-role": "Teams.Activity.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "Type", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "UseReportDB", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/ListTeamsLisLocation": { + "get": { + "summary": "Lists Teams emergency calling Location Information Service (LIS) locations configured for a tenant.", + "tags": [ + "Teams-Sharepoint" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Lists Teams emergency calling Location Information Service (LIS) locations configured for a tenant.", + "x-cipp-role": "Teams.Voice.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListTeamsVoice": { + "get": { + "summary": "Lists Microsoft Teams voice and PSTN usage for a tenant.", + "tags": [ + "Teams-Sharepoint" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Lists Microsoft Teams voice and PSTN usage for a tenant. Supports UseReportDB=true query parameter to retrieve cached data from the reporting database for significantly better performance, especially when querying AllTenants.", + "x-cipp-role": "Teams.Voice.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "UseReportDB", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/ListTenantAlignment": { + "get": { + "summary": "Lists tenant alignment data showing how well tenants conform to their assigned standards templates.", + "tags": [ + "Tenant > Standards" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Lists tenant alignment data showing how well tenants conform to their assigned standards templates.", + "x-cipp-role": "Tenant.Standards.Read", + "parameters": [ + { + "name": "granular", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/ListTenantAllowBlockList": { + "get": { + "summary": "Lists Tenant Allow/Block List entries (senders, URLs, file hashes, IPs) from Exchange Online Protection.", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Lists Tenant Allow/Block List entries (senders, URLs, file hashes, IPs) from Exchange Online Protection.", + "x-cipp-role": "Exchange.SpamFilter.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListTenantAllowBlockListTemplates": { + "get": { + "summary": "Lists saved Tenant Allow/Block List templates for Exchange Online Protection.", + "tags": [ + "Email-Exchange > Spamfilter" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Lists saved Tenant Allow/Block List templates for Exchange Online Protection.", + "x-cipp-role": "Exchange.SpamFilter.Read", + "parameters": [ + { + "name": "ID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/ListTenantDetails": { + "get": { + "summary": "Retrieves detailed organization information for a tenant, including addresses, assigned plans, sync status, and custo...", + "tags": [ + "Tenant > Administration > Tenant" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Retrieves detailed organization information for a tenant, including addresses, assigned plans, sync status, and custom properties.", + "x-cipp-role": "CIPP.Core.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListTenantDrift": { + "get": { + "summary": "Lists configuration drift for tenants, comparing current state against the desired state defined by applied standards.", + "tags": [ + "Tenant > Standards" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Lists configuration drift for tenants, comparing current state against the desired state defined by applied standards.", + "x-cipp-role": "Tenant.Standards.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListTenantGroups": { + "post": { + "summary": "Entrypoint for listing tenant groups", + "tags": [ + "CIPP > Settings" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Lists tenant groups (logical groupings of managed tenants) and optionally includes usage data.", + "x-cipp-role": "CIPP.Core.Read", + "parameters": [ + { + "name": "groupId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "includeUsage", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "groupId": { + "type": "string" + }, + "includeUsage": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ListTenantOnboarding": { + "get": { + "summary": "Lists tenant onboarding requests and their step-by-step progress, including GDAP relationship setup.", + "tags": [ + "Tenant > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Lists tenant onboarding requests and their step-by-step progress, including GDAP relationship setup.", "x-cipp-role": "Tenant.Administration.Read", "requestBody": { "required": true, @@ -30759,19 +37076,24 @@ "type": "object", "properties": { "standardsExcludeAllTenants": { - "type": "string" + "type": "string", + "description": "(auto) detected in frontend form/call" }, "id": { - "type": "string" + "type": "string", + "description": "(auto) detected in frontend form/call" }, "remapRoles": { - "type": "string" + "type": "string", + "description": "(auto) detected in frontend form/call" }, "gdapRoles": { - "type": "string" + "type": "string", + "description": "(auto) detected in frontend form/call" }, "ignoreMissingRoles": { - "type": "string" + "type": "string", + "description": "(auto) detected in frontend form/call" } } } @@ -30782,7 +37104,7 @@ }, "/api/ListTenants": { "post": { - "summary": "ListTenants", + "summary": "Lists all managed tenants accessible to the current user, with support for cache clearing and tenant filtering.", "tags": [ "Tenant > Administration > Tenant" ], @@ -30803,18 +37125,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists all managed tenants accessible to the current user, with support for cache clearing and tenant filtering. This is the primary endpoint for tenant enumeration.", "x-cipp-role": "CIPP.Core.Read", "parameters": [ { @@ -30822,7 +37145,8 @@ "in": "query", "required": false, "schema": { - "type": "string" + "type": "string", + "description": "AllTenantSelector parameter" } }, { @@ -30830,7 +37154,8 @@ "in": "query", "required": false, "schema": { - "type": "string" + "type": "string", + "description": "IncludeOffboardingDefaults parameter" } }, { @@ -30838,7 +37163,8 @@ "in": "query", "required": false, "schema": { - "type": "string" + "type": "string", + "description": "Mode parameter" } }, { @@ -30846,7 +37172,8 @@ "in": "query", "required": false, "schema": { - "type": "string" + "type": "string", + "description": "TriggerRefresh parameter" } }, { @@ -30861,13 +37188,20 @@ "type": "object", "properties": { "ClearCache": { - "type": "string" + "type": "string", + "description": "ClearCache parameter" }, "integrationCompany": { - "$ref": "#/components/schemas/LabelValue" + "allOf": [ + { + "$ref": "#/components/schemas/LabelValue" + } + ], + "description": "integrationCompany parameter" }, "TenantsOnly": { - "type": "string" + "type": "string", + "description": "TenantsOnly parameter" } } } @@ -30899,13 +37233,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -30937,13 +37271,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -30988,7 +37322,7 @@ }, "/api/ListTransportRules": { "get": { - "summary": "ListTransportRules", + "summary": "Lists mail flow (transport) rules configured in Exchange Online, with optional detailed view by rule ID.", "tags": [ "Email-Exchange > Transport" ], @@ -31009,18 +37343,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists mail flow (transport) rules configured in Exchange Online, with optional detailed view by rule ID.", "x-cipp-role": "Exchange.TransportRule.Read", "parameters": [ { @@ -31039,7 +37374,7 @@ }, "/api/ListTransportRulesTemplates": { "get": { - "summary": "ListTransportRulesTemplates", + "summary": "Lists saved transport rule templates for Exchange Online mail flow rules.", "tags": [ "Email-Exchange > Transport" ], @@ -31060,18 +37395,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists saved transport rule templates for Exchange Online mail flow rules.", "x-cipp-role": "Exchange.TransportRule.Read", "parameters": [ { @@ -31081,13 +37417,21 @@ "schema": { "type": "string" } + }, + { + "name": "noJson", + "in": "query", + "required": false, + "schema": { + "type": "string" + } } ] } }, "/api/ListUserConditionalAccessPolicies": { "get": { - "summary": "ListUserConditionalAccessPolicies", + "summary": "Lists Conditional Access policies that apply to a specific user in a tenant.", "tags": [ "Identity > Administration > Users" ], @@ -31108,18 +37452,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists Conditional Access policies that apply to a specific user in a tenant.", "x-cipp-role": "Identity.User.Read", "parameters": [ { @@ -31138,7 +37483,7 @@ }, "/api/ListUserCounts": { "get": { - "summary": "ListUserCounts", + "summary": "Returns summary counts of total users, licensed users, and global administrators for a tenant.", "tags": [ "Identity > Administration > Users" ], @@ -31159,18 +37504,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Returns summary counts of total users, licensed users, and global administrators for a tenant.", "x-cipp-role": "Identity.User.Read", "parameters": [ { @@ -31181,7 +37527,7 @@ }, "/api/ListUserDevices": { "get": { - "summary": "ListUserDevices", + "summary": "Lists Intune-managed devices registered to a specific user, including compliance status and endpoint protection details.", "tags": [ "Identity > Administration > Users" ], @@ -31202,18 +37548,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists Intune-managed devices registered to a specific user, including compliance status and endpoint protection details.", "x-cipp-role": "Identity.User.Read", "parameters": [ { @@ -31232,7 +37579,7 @@ }, "/api/ListUserGroups": { "get": { - "summary": "ListUserGroups", + "summary": "Lists Entra ID group memberships for a specific user.", "tags": [ "Identity > Administration > Users" ], @@ -31253,18 +37600,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists Entra ID group memberships for a specific user.", "x-cipp-role": "Identity.User.Read", "parameters": [ { @@ -31283,7 +37631,7 @@ }, "/api/ListUserMailboxDetails": { "get": { - "summary": "ListUserMailboxDetails", + "summary": "Retrieves detailed Exchange Online mailbox properties for a specific user, including quotas, archive status, and prot...", "tags": [ "Identity > Administration > Users" ], @@ -31304,18 +37652,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Retrieves detailed Exchange Online mailbox properties for a specific user, including quotas, archive status, and protocols.", "x-cipp-role": "Exchange.Mailbox.Read", "parameters": [ { @@ -31342,7 +37691,7 @@ }, "/api/ListUserMailboxRules": { "get": { - "summary": "ListUserMailboxRules", + "summary": "Lists inbox rules configured on a specific user's Exchange Online mailbox.", "tags": [ "Identity > Administration > Users" ], @@ -31363,18 +37712,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists inbox rules configured on a specific user's Exchange Online mailbox.", "x-cipp-role": "Exchange.Mailbox.Read", "x-cipp-dynamic-options": true, "parameters": [ @@ -31402,7 +37752,7 @@ }, "/api/ListUserPhoto": { "get": { - "summary": "ListUserPhoto", + "summary": "Retrieves the profile photo for a specific Entra ID user, returned as a base64-encoded image.", "tags": [ "Identity > Administration > Users" ], @@ -31423,18 +37773,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Retrieves the profile photo for a specific Entra ID user, returned as a base64-encoded image.", "x-cipp-role": "Identity.User.Read", "parameters": [ { @@ -31453,7 +37804,46 @@ }, "/api/ListUserSettings": { "get": { - "summary": "ListUserSettings", + "summary": "Retrieves the current CIPP user's personal settings and preferences.", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Retrieves the current CIPP user's personal settings and preferences.", + "x-cipp-role": "Identity.User.Read" + } + }, + "/api/ListUserSigninLogs": { + "get": { + "summary": "Lists recent sign-in log entries for a specific Entra ID user, ordered by most recent.", "tags": [ "Identity > Administration > Users" ], @@ -31472,28 +37862,1111 @@ } } } - }, - "400": { - "description": "Bad request — missing required field or invalid input" - }, - "401": { - "description": "Unauthorized — invalid or missing bearer token" - }, - "403": { - "description": "Forbidden — caller lacks the required RBAC role" - }, - "500": { - "description": "Internal server error" + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Lists recent sign-in log entries for a specific Entra ID user, ordered by most recent.", + "x-cipp-role": "Identity.User.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "top", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "UserID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/ListUserTrustedBlockedSenders": { + "get": { + "summary": "Lists trusted and blocked sender entries configured for a specific user's Exchange Online mailbox junk email settings.", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Lists trusted and blocked sender entries configured for a specific user's Exchange Online mailbox junk email settings.", + "x-cipp-role": "Exchange.Mailbox.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "UserID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "userPrincipalName", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/ListUsers": { + "get": { + "summary": "Lists Entra ID users for a tenant with license and sign-in details, or retrieves a specific user by ID.", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Lists Entra ID users for a tenant with license and sign-in details, or retrieves a specific user by ID. For AllTenants or cached data, consider using ListDBCache with type=Users for significantly better performance.", + "x-cipp-role": "Identity.User.Read", + "parameters": [ + { + "name": "graphFilter", + "in": "query", + "required": false, + "schema": { + "type": "string", + "description": "OData $filter expression passed to Graph" + } + }, + { + "name": "IncludeLogonDetails", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "UserID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/ListUsersAndGroups": { + "get": { + "summary": "Lists both users and groups for a tenant in a single batch call, returning ID and display name for selection/lookup p...", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Lists both users and groups for a tenant in a single batch call, returning ID and display name for selection/lookup purposes.", + "x-cipp-role": "Tenant.Directory.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/ListWebhookAlert": { + "get": { + "summary": "Lists configured webhook alert rules that trigger on specific audit log events.", + "tags": [ + "Tenant > Administration > Alerts" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Lists configured webhook alert rules that trigger on specific audit log events.", + "x-cipp-role": "CIPP.Alert.Read" + } + }, + "/api/ListWorkerHealth": { + "post": { + "summary": "Retrieves health status and diagnostics for CIPP background worker processes.", + "tags": [ + "CIPP > Settings" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Retrieves health status and diagnostics for CIPP background worker processes. Requires SuperAdmin access.", + "x-cipp-role": "CIPP.SuperAdmin.Read", + "parameters": [ + { + "name": "Action", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "JobId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Limit", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "MaxPoints", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Minutes", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "PoolType", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Priority", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "RunName", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Status", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "JobId": { + "type": "string" + }, + "Priority": { + "type": "string" + }, + "RunName": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/ListmailboxPermissions": { + "get": { + "summary": "Lists mailbox permissions (Full Access, Send As, Send on Behalf) for a tenant.", + "tags": [ + "Email-Exchange > Administration" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Lists mailbox permissions (Full Access, Send As, Send on Behalf) for a tenant. Supports UseReportDB=true query parameter to retrieve cached data from the reporting database for significantly better performance, especially when querying AllTenants.", + "x-cipp-role": "Exchange.Mailbox.Read", + "parameters": [ + { + "name": "ByUser", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "UseReportDB", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "userId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/PatchUser": { + "patch": { + "summary": "PatchUser", + "tags": [ + "Identity > Administration > Users" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Identity.User.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true, + "properties": { + "id": { + "type": "string" + }, + "manager": { + "type": "string", + "description": "Required field on each array element (validated in handler)." + }, + "sponsor": { + "type": "string", + "description": "Required field on each array element (validated in handler)." + }, + "tenantFilter": { + "type": "string", + "description": "Required field on each array element (validated in handler)." + } + }, + "required": [ + "tenantFilter" + ] + } + }, + { + "type": "object", + "additionalProperties": true, + "properties": { + "id": { + "type": "string" + }, + "manager": { + "type": "string", + "description": "Required field on each array element (validated in handler)." + }, + "sponsor": { + "type": "string", + "description": "Required field on each array element (validated in handler)." + }, + "tenantFilter": { + "type": "string", + "description": "Required field on each array element (validated in handler)." + } + }, + "required": [ + "tenantFilter" + ] + } + ], + "description": "Accepts a single object or an array of objects. Each object must include the required fields." + } + } + } + } + } + }, + "/api/PublicPhishingCheck": { + "post": { + "summary": "PublicPhishingCheck", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Public", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "AlertMessage": { + "type": "string" + }, + "Cloned": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "score": { + "type": "string" + }, + "source": { + "type": "string" + }, + "TenantId": { + "type": "string" + }, + "threshold": { + "type": "string" + }, + "type": { + "type": "string" + }, + "url": { + "type": "string" + }, + "userDisplayName": { + "type": "string" + }, + "userEmail": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/PublicPing": { + "get": { + "summary": "PublicPing", + "tags": [ + "CIPP > Core" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Public" + } + }, + "/api/PublicWebhooks": { + "post": { + "summary": "PublicWebhooks", + "tags": [ + "Tenant > Administration > Alerts" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Public", + "parameters": [ + { + "name": "CIPPID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Type", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "validationCode", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "ValidationToken", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "validationCode": { + "type": "string" + }, + "value": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/RemoveAPDevice": { + "post": { + "summary": "RemoveAPDevice", + "tags": [ + "Endpoint > Autopilot" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.Autopilot.ReadWrite", + "parameters": [ + { + "name": "ID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ID": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/RemoveApp": { + "post": { + "summary": "RemoveApp", + "tags": [ + "Endpoint > Applications" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.Application.ReadWrite", + "parameters": [ + { + "name": "ID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ID": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } + } + }, + "/api/RemoveAppTemplate": { + "post": { + "summary": "RemoveAppTemplate", + "tags": [ + "Endpoint > Applications" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.Application.ReadWrite", + "parameters": [ + { + "name": "ID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ID": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/RemoveAssignmentFilterTemplate": { + "post": { + "summary": "RemoveAssignmentFilterTemplate", + "tags": [ + "Endpoint > MEM" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.MEM.ReadWrite", + "parameters": [ + { + "name": "ID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ID": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/RemoveAutopilotConfig": { + "post": { + "summary": "RemoveAutopilotConfig", + "tags": [ + "Endpoint > Autopilot" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Endpoint.Autopilot.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "assignments": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "ID": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } } - }, - "x-cipp-role": "Identity.User.Read" + } } }, - "/api/ListUserSigninLogs": { - "get": { - "summary": "ListUserSigninLogs", + "/api/RemoveBPATemplate": { + "post": { + "summary": "RemoveBPATemplate", "tags": [ - "Identity > Administration > Users" + "Tenant > Standards" ], "security": [ { @@ -31512,47 +38985,51 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Identity.User.Read", + "x-cipp-role": "Tenant.Standards.ReadWrite", "parameters": [ { - "$ref": "#/components/parameters/tenantFilter" - }, - { - "name": "top", + "name": "TemplateName", "in": "query", "required": false, "schema": { "type": "string" } - }, - { - "name": "UserID", - "in": "query", - "required": false, - "schema": { - "type": "string" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "TemplateName": { + "type": "string" + } + } + } } } - ] + } } }, - "/api/ListUserTrustedBlockedSenders": { - "get": { - "summary": "ListUserTrustedBlockedSenders", + "/api/RemoveCAPolicy": { + "post": { + "summary": "RemoveCAPolicy", "tags": [ - "Identity > Administration > Users" + "Tenant > Conditional" ], "security": [ { @@ -31571,25 +39048,22 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Exchange.Mailbox.Read", + "x-cipp-role": "Tenant.ConditionalAccess.ReadWrite", "parameters": [ { - "$ref": "#/components/parameters/tenantFilter" - }, - { - "name": "UserID", + "name": "GUID", "in": "query", "required": false, "schema": { @@ -31597,21 +39071,37 @@ } }, { - "name": "userPrincipalName", - "in": "query", - "required": false, - "schema": { - "type": "string" + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "GUID": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } } } - ] + } } }, - "/api/ListUsers": { - "get": { - "summary": "ListUsers", + "/api/RemoveCATemplate": { + "post": { + "summary": "RemoveCATemplate", "tags": [ - "Identity > Administration > Users" + "Tenant > Conditional" ], "security": [ { @@ -31630,54 +39120,49 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Identity.User.Read", + "x-cipp-role": "Tenant.ConditionalAccess.ReadWrite", "parameters": [ { - "name": "graphFilter", - "in": "query", - "required": false, - "schema": { - "type": "string", - "description": "OData $filter expression passed to Graph" - } - }, - { - "name": "IncludeLogonDetails", + "name": "ID", "in": "query", "required": false, "schema": { "type": "string" } - }, - { - "$ref": "#/components/parameters/tenantFilter" - }, - { - "name": "UserID", - "in": "query", - "required": false, - "schema": { - "type": "string" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ID": { + "type": "string" + } + } + } } } - ] + } } }, - "/api/ListUsersAndGroups": { + "/api/RemoveCippQueue": { "get": { - "summary": "ListUsersAndGroups", + "summary": "RemoveCippQueue", "tags": [ "Uncategorized" ], @@ -31698,31 +39183,26 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Tenant.Directory.Read", - "parameters": [ - { - "$ref": "#/components/parameters/tenantFilter" - } - ] + "x-cipp-role": "CIPP.Core.ReadWrite" } }, - "/api/ListWebhookAlert": { - "get": { - "summary": "ListWebhookAlert", + "/api/RemoveConnectionfilterTemplate": { + "post": { + "summary": "RemoveConnectionfilterTemplate", "tags": [ - "Tenant > Administration > Alerts" + "Email-Exchange > Spamfilter" ], "security": [ { @@ -31741,26 +39221,41 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "CIPP.Alert.Read" + "x-cipp-role": "Exchange.ConnectionFilter.ReadWrite", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ID": { + "type": "string" + } + } + } + } + } + } } }, - "/api/ListmailboxPermissions": { - "get": { - "summary": "ListmailboxPermissions", + "/api/RemoveContact": { + "post": { + "summary": "RemoveContact", "tags": [ - "Email-Exchange > Administration" + "Email-Exchange > Administration > Contacts" ], "security": [ { @@ -31779,22 +39274,22 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Exchange.Mailbox.Read", + "x-cipp-role": "Exchange.Contact.ReadWrite", "parameters": [ { - "name": "ByUser", + "name": "GUID", "in": "query", "required": false, "schema": { @@ -31802,10 +39297,7 @@ } }, { - "$ref": "#/components/parameters/tenantFilter" - }, - { - "name": "UseReportDB", + "name": "Mail", "in": "query", "required": false, "schema": { @@ -31813,21 +39305,40 @@ } }, { - "name": "userId", - "in": "query", - "required": false, - "schema": { - "type": "string" + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "GUID": { + "type": "string" + }, + "Mail": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } } } - ] + } } }, - "/api/PatchUser": { - "patch": { - "summary": "PatchUser", + "/api/RemoveContactTemplates": { + "post": { + "summary": "RemoveContactTemplates", "tags": [ - "Identity > Administration > Users" + "Email-Exchange > Administration > Contacts" ], "security": [ { @@ -31846,73 +39357,51 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Identity.User.ReadWrite", + "x-cipp-role": "Exchange.Contact.ReadWrite", + "parameters": [ + { + "name": "ID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], "requestBody": { "required": true, "content": { "application/json": { "schema": { - "oneOf": [ - { - "type": "array", - "items": { - "type": "object", - "additionalProperties": true, - "properties": { - "id": { - "type": "string" - }, - "tenantFilter": { - "type": "string", - "description": "Required field on each array element (validated in handler)." - } - }, - "required": [ - "tenantFilter" - ] - } - }, - { - "type": "object", - "additionalProperties": true, - "properties": { - "id": { - "type": "string" - }, - "tenantFilter": { - "type": "string", - "description": "Required field on each array element (validated in handler)." - } - }, - "required": [ - "tenantFilter" - ] + "type": "object", + "properties": { + "ID": { + "type": "string" } - ], - "description": "Accepts a single object or an array of objects. Each object must include the required fields." + } } } } } } }, - "/api/PublicPhishingCheck": { + "/api/RemoveCustomScript": { "post": { - "summary": "PublicPhishingCheck", + "summary": "RemoveCustomScript", "tags": [ - "Uncategorized" + "Tools > Custom-Scripts" ], "security": [ { @@ -31931,19 +39420,29 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Public", + "x-cipp-role": "CIPP.Tests.ReadWrite", + "parameters": [ + { + "name": "ScriptGuid", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], "requestBody": { "required": true, "content": { @@ -31951,37 +39450,7 @@ "schema": { "type": "object", "properties": { - "AlertMessage": { - "type": "string" - }, - "Cloned": { - "type": "string" - }, - "reason": { - "type": "string" - }, - "score": { - "type": "string" - }, - "source": { - "type": "string" - }, - "TenantId": { - "type": "string" - }, - "threshold": { - "type": "string" - }, - "type": { - "type": "string" - }, - "url": { - "type": "string" - }, - "userDisplayName": { - "type": "string" - }, - "userEmail": { + "ScriptGuid": { "type": "string" } } @@ -31991,11 +39460,11 @@ } } }, - "/api/PublicPing": { - "get": { - "summary": "PublicPing", + "/api/RemoveDeletedObject": { + "post": { + "summary": "RemoveDeletedObject", "tags": [ - "CIPP > Core" + "Identity > Administration > Users" ], "security": [ { @@ -32014,26 +39483,66 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Public" + "x-cipp-role": "Tenant.Directory.ReadWrite", + "parameters": [ + { + "name": "ID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "displayName": { + "type": "string" + }, + "ID": { + "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "userPrincipalName": { + "type": "string" + } + }, + "required": [ + "tenantFilter" + ] + } + } + } + } } }, - "/api/PublicWebhooks": { + "/api/RemoveDlpCompliancePolicy": { "post": { - "summary": "PublicWebhooks", + "summary": "RemoveDlpCompliancePolicy", "tags": [ - "Tenant > Administration > Alerts" + "Security > Compliance-DLP" ], "security": [ { @@ -32052,38 +39561,22 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Public", + "x-cipp-role": "Security.DlpCompliancePolicy.ReadWrite", "parameters": [ { - "name": "CIPPID", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "Type", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "validationCode", + "name": "Identity", "in": "query", "required": false, "schema": { @@ -32091,12 +39584,7 @@ } }, { - "name": "ValidationToken", - "in": "query", - "required": false, - "schema": { - "type": "string" - } + "$ref": "#/components/parameters/tenantFilter" } ], "requestBody": { @@ -32106,24 +39594,30 @@ "schema": { "type": "object", "properties": { - "validationCode": { + "Identity": { "type": "string" }, - "value": { + "Name": { + "type": "string" + }, + "tenantFilter": { "type": "string" } - } + }, + "required": [ + "tenantFilter" + ] } } } } } }, - "/api/RemoveAPDevice": { + "/api/RemoveDlpCompliancePolicyTemplate": { "post": { - "summary": "RemoveAPDevice", + "summary": "RemoveDlpCompliancePolicyTemplate", "tags": [ - "Endpoint > Autopilot" + "Security > Compliance-DLP" ], "security": [ { @@ -32142,19 +39636,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Endpoint.Autopilot.ReadWrite", + "x-cipp-role": "Security.DlpCompliancePolicy.ReadWrite", "parameters": [ { "name": "ID", @@ -32163,9 +39657,6 @@ "schema": { "type": "string" } - }, - { - "$ref": "#/components/parameters/tenantFilter" } ], "requestBody": { @@ -32177,25 +39668,19 @@ "properties": { "ID": { "type": "string" - }, - "tenantFilter": { - "type": "string" } - }, - "required": [ - "tenantFilter" - ] + } } } } } } }, - "/api/RemoveApp": { + "/api/RemoveExConnector": { "post": { - "summary": "RemoveApp", + "summary": "RemoveExConnector", "tags": [ - "Endpoint > Applications" + "Email-Exchange > Transport" ], "security": [ { @@ -32214,22 +39699,22 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Endpoint.Application.ReadWrite", + "x-cipp-role": "Exchange.Connector.ReadWrite", "parameters": [ { - "name": "ID", + "name": "GUID", "in": "query", "required": false, "schema": { @@ -32238,6 +39723,14 @@ }, { "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "Type", + "in": "query", + "required": false, + "schema": { + "type": "string" + } } ], "requestBody": { @@ -32247,11 +39740,14 @@ "schema": { "type": "object", "properties": { - "ID": { + "GUID": { "type": "string" }, "tenantFilter": { "type": "string" + }, + "Type": { + "type": "string" } }, "required": [ @@ -32263,11 +39759,11 @@ } } }, - "/api/RemoveAssignmentFilterTemplate": { + "/api/RemoveExConnectorTemplate": { "post": { - "summary": "RemoveAssignmentFilterTemplate", + "summary": "RemoveExConnectorTemplate", "tags": [ - "Endpoint > MEM" + "Email-Exchange > Transport" ], "security": [ { @@ -32286,19 +39782,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Endpoint.MEM.ReadWrite", + "x-cipp-role": "Exchange.Connector.ReadWrite", "parameters": [ { "name": "ID", @@ -32326,11 +39822,11 @@ } } }, - "/api/RemoveAutopilotConfig": { + "/api/RemoveGroupTemplate": { "post": { - "summary": "RemoveAutopilotConfig", + "summary": "RemoveGroupTemplate", "tags": [ - "Endpoint > Autopilot" + "Identity > Administration > Groups" ], "security": [ { @@ -32349,19 +39845,29 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Endpoint.Autopilot.ReadWrite", + "x-cipp-role": "Identity.Group.ReadWrite", + "parameters": [ + { + "name": "ID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], "requestBody": { "required": true, "content": { @@ -32369,33 +39875,21 @@ "schema": { "type": "object", "properties": { - "assignments": { - "type": "string" - }, - "displayName": { - "type": "string" - }, "ID": { "type": "string" - }, - "tenantFilter": { - "type": "string" } - }, - "required": [ - "tenantFilter" - ] + } } } } } } }, - "/api/RemoveBPATemplate": { + "/api/RemoveIntuneReusableSetting": { "post": { - "summary": "RemoveBPATemplate", + "summary": "RemoveIntuneReusableSetting", "tags": [ - "Tenant > Standards" + "Endpoint > MEM" ], "security": [ { @@ -32414,27 +39908,38 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Tenant.Standards.ReadWrite", + "x-cipp-role": "Endpoint.MEM.ReadWrite", "parameters": [ { - "name": "TemplateName", + "name": "DisplayName", "in": "query", "required": false, "schema": { "type": "string" } + }, + { + "name": "ID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" } ], "requestBody": { @@ -32444,21 +39949,30 @@ "schema": { "type": "object", "properties": { - "TemplateName": { + "DisplayName": { + "type": "string" + }, + "ID": { + "type": "string" + }, + "tenantFilter": { "type": "string" } - } + }, + "required": [ + "tenantFilter" + ] } } } } } }, - "/api/RemoveCAPolicy": { + "/api/RemoveIntuneReusableSettingTemplate": { "post": { - "summary": "RemoveCAPolicy", + "summary": "RemoveIntuneReusableSettingTemplate", "tags": [ - "Tenant > Conditional" + "Endpoint > MEM" ], "security": [ { @@ -32477,30 +39991,27 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Tenant.ConditionalAccess.ReadWrite", + "x-cipp-role": "Endpoint.MEM.ReadWrite", "parameters": [ { - "name": "GUID", + "name": "ID", "in": "query", "required": false, "schema": { "type": "string" } - }, - { - "$ref": "#/components/parameters/tenantFilter" } ], "requestBody": { @@ -32510,27 +40021,21 @@ "schema": { "type": "object", "properties": { - "GUID": { - "type": "string" - }, - "tenantFilter": { + "ID": { "type": "string" } - }, - "required": [ - "tenantFilter" - ] + } } } } } } }, - "/api/RemoveCATemplate": { + "/api/RemoveIntuneScript": { "post": { - "summary": "RemoveCATemplate", + "summary": "RemoveIntuneScript", "tags": [ - "Tenant > Conditional" + "Endpoint > MEM" ], "security": [ { @@ -32549,29 +40054,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Tenant.ConditionalAccess.ReadWrite", - "parameters": [ - { - "name": "ID", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - } - ], + "x-cipp-role": "Endpoint.MEM.ReadWrite", "requestBody": { "required": true, "content": { @@ -32579,21 +40074,33 @@ "schema": { "type": "object", "properties": { + "DisplayName": { + "type": "string" + }, "ID": { "type": "string" + }, + "ScriptType": { + "type": "string" + }, + "TenantFilter": { + "type": "string" } - } + }, + "required": [ + "TenantFilter" + ] } } } } } }, - "/api/RemoveConnectionfilterTemplate": { + "/api/RemoveIntuneTemplate": { "post": { - "summary": "RemoveConnectionfilterTemplate", + "summary": "RemoveIntuneTemplate", "tags": [ - "Email-Exchange > Spamfilter" + "Endpoint > MEM" ], "security": [ { @@ -32612,19 +40119,29 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Exchange.ConnectionFilter.ReadWrite", + "x-cipp-role": "Endpoint.MEM.ReadWrite", + "parameters": [ + { + "name": "ID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], "requestBody": { "required": true, "content": { @@ -32642,11 +40159,11 @@ } } }, - "/api/RemoveContact": { + "/api/RemoveJITAdminTemplate": { "post": { - "summary": "RemoveContact", + "summary": "RemoveJITAdminTemplate", "tags": [ - "Email-Exchange > Administration > Contacts" + "Identity > Administration > Users" ], "security": [ { @@ -32665,38 +40182,27 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Exchange.Contact.ReadWrite", + "x-cipp-role": "Identity.Role.ReadWrite", "parameters": [ { - "name": "GUID", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "Mail", + "name": "ID", "in": "query", "required": false, "schema": { "type": "string" } - }, - { - "$ref": "#/components/parameters/tenantFilter" } ], "requestBody": { @@ -32706,30 +40212,21 @@ "schema": { "type": "object", "properties": { - "GUID": { - "type": "string" - }, - "Mail": { - "type": "string" - }, - "tenantFilter": { + "ID": { "type": "string" } - }, - "required": [ - "tenantFilter" - ] + } } } } } } }, - "/api/RemoveContactTemplates": { + "/api/RemovePolicy": { "post": { - "summary": "RemoveContactTemplates", + "summary": "RemovePolicy", "tags": [ - "Email-Exchange > Administration > Contacts" + "Endpoint > MEM" ], "security": [ { @@ -32748,19 +40245,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Exchange.Contact.ReadWrite", + "x-cipp-role": "Endpoint.MEM.ReadWrite", "parameters": [ { "name": "ID", @@ -32769,6 +40266,17 @@ "schema": { "type": "string" } + }, + { + "$ref": "#/components/parameters/tenantFilter" + }, + { + "name": "URLName", + "in": "query", + "required": false, + "schema": { + "type": "string" + } } ], "requestBody": { @@ -32780,19 +40288,28 @@ "properties": { "ID": { "type": "string" + }, + "tenantFilter": { + "type": "string" + }, + "URLName": { + "type": "string" } - } + }, + "required": [ + "tenantFilter" + ] } } } } } }, - "/api/RemoveDeletedObject": { + "/api/RemoveQuarantinePolicy": { "post": { - "summary": "RemoveDeletedObject", + "summary": "RemoveQuarantinePolicy", "tags": [ - "Identity > Administration > Users" + "Email-Exchange > Spamfilter" ], "security": [ { @@ -32811,22 +40328,30 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Tenant.Directory.ReadWrite", + "x-cipp-role": "Exchange.Spamfilter.ReadWrite", "parameters": [ { - "name": "ID", + "name": "Identity", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Name", "in": "query", "required": false, "schema": { @@ -32844,21 +40369,18 @@ "schema": { "type": "object", "properties": { - "displayName": { - "type": "string" - }, - "ID": { + "Identity": { "type": "string" }, - "tenantFilter": { + "Name": { "type": "string" }, - "userPrincipalName": { + "TenantFilter": { "type": "string" } }, "required": [ - "tenantFilter" + "TenantFilter" ] } } @@ -32866,11 +40388,11 @@ } } }, - "/api/RemoveExConnector": { + "/api/RemoveQueuedAlert": { "post": { - "summary": "RemoveExConnector", + "summary": "RemoveQueuedAlert", "tags": [ - "Email-Exchange > Transport" + "Tenant > Administration > Alerts" ], "security": [ { @@ -32889,22 +40411,22 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Exchange.Connector.ReadWrite", + "x-cipp-role": "CIPP.Alert.ReadWrite", "parameters": [ { - "name": "GUID", + "name": "EventType", "in": "query", "required": false, "schema": { @@ -32912,10 +40434,7 @@ } }, { - "$ref": "#/components/parameters/tenantFilter" - }, - { - "name": "Type", + "name": "ID", "in": "query", "required": false, "schema": { @@ -32930,30 +40449,24 @@ "schema": { "type": "object", "properties": { - "GUID": { - "type": "string" - }, - "tenantFilter": { + "EventType": { "type": "string" }, - "Type": { + "ID": { "type": "string" } - }, - "required": [ - "tenantFilter" - ] + } } } } } } }, - "/api/RemoveExConnectorTemplate": { + "/api/RemoveQueuedApp": { "post": { - "summary": "RemoveExConnectorTemplate", + "summary": "RemoveQueuedApp", "tags": [ - "Email-Exchange > Transport" + "Endpoint > Applications" ], "security": [ { @@ -32972,29 +40485,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Exchange.Connector.ReadWrite", - "parameters": [ - { - "name": "ID", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - } - ], + "x-cipp-role": "Endpoint.Application.ReadWrite", "requestBody": { "required": true, "content": { @@ -33012,11 +40515,11 @@ } } }, - "/api/RemoveGroupTemplate": { + "/api/RemoveRetentionCompliancePolicy": { "post": { - "summary": "RemoveGroupTemplate", + "summary": "RemoveRetentionCompliancePolicy", "tags": [ - "Identity > Administration > Groups" + "Security > Compliance-Retention" ], "security": [ { @@ -33035,27 +40538,30 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Identity.Group.ReadWrite", + "x-cipp-role": "Security.RetentionCompliancePolicy.ReadWrite", "parameters": [ { - "name": "ID", + "name": "Identity", "in": "query", "required": false, "schema": { "type": "string" } + }, + { + "$ref": "#/components/parameters/tenantFilter" } ], "requestBody": { @@ -33065,21 +40571,33 @@ "schema": { "type": "object", "properties": { - "ID": { + "ForceDeletion": { + "type": "string" + }, + "Identity": { + "type": "string" + }, + "Name": { + "type": "string" + }, + "tenantFilter": { "type": "string" } - } + }, + "required": [ + "tenantFilter" + ] } } } } } }, - "/api/RemoveIntuneReusableSetting": { + "/api/RemoveRetentionCompliancePolicyTemplate": { "post": { - "summary": "RemoveIntuneReusableSetting", + "summary": "RemoveRetentionCompliancePolicyTemplate", "tags": [ - "Endpoint > MEM" + "Security > Compliance-Retention" ], "security": [ { @@ -33098,28 +40616,20 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Endpoint.MEM.ReadWrite", + "x-cipp-role": "Security.RetentionCompliancePolicy.ReadWrite", "parameters": [ - { - "name": "DisplayName", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, { "name": "ID", "in": "query", @@ -33127,9 +40637,6 @@ "schema": { "type": "string" } - }, - { - "$ref": "#/components/parameters/tenantFilter" } ], "requestBody": { @@ -33139,30 +40646,21 @@ "schema": { "type": "object", "properties": { - "DisplayName": { - "type": "string" - }, "ID": { "type": "string" - }, - "tenantFilter": { - "type": "string" } - }, - "required": [ - "tenantFilter" - ] + } } } } } } }, - "/api/RemoveIntuneReusableSettingTemplate": { + "/api/RemoveSafeLinksPolicyTemplate": { "post": { - "summary": "RemoveIntuneReusableSettingTemplate", + "summary": "RemoveSafeLinksPolicyTemplate", "tags": [ - "Endpoint > MEM" + "Security > Safe-Links-Policy" ], "security": [ { @@ -33181,19 +40679,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Endpoint.MEM.ReadWrite", + "x-cipp-role": "Exchange.SafeLinks.ReadWrite", "parameters": [ { "name": "ID", @@ -33221,76 +40719,11 @@ } } }, - "/api/RemoveIntuneScript": { - "post": { - "summary": "RemoveIntuneScript", - "tags": [ - "Endpoint > MEM" - ], - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StandardResults" - } - } - } - }, - "400": { - "description": "Bad request — missing required field or invalid input" - }, - "401": { - "description": "Unauthorized — invalid or missing bearer token" - }, - "403": { - "description": "Forbidden — caller lacks the required RBAC role" - }, - "500": { - "description": "Internal server error" - } - }, - "x-cipp-role": "Endpoint.MEM.ReadWrite", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "DisplayName": { - "type": "string" - }, - "ID": { - "type": "string" - }, - "ScriptType": { - "type": "string" - }, - "TenantFilter": { - "type": "string" - } - }, - "required": [ - "TenantFilter" - ] - } - } - } - } - } - }, - "/api/RemoveIntuneTemplate": { + "/api/RemoveScheduledItem": { "post": { - "summary": "RemoveIntuneTemplate", + "summary": "Removes a scheduled item from CIPP's scheduler.", "tags": [ - "Endpoint > MEM" + "CIPP > Scheduler" ], "security": [ { @@ -33309,22 +40742,23 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Endpoint.MEM.ReadWrite", + "description": "Removes a scheduled item from CIPP's scheduler.", + "x-cipp-role": "CIPP.Scheduler.ReadWrite", "parameters": [ { - "name": "ID", + "name": "id", "in": "query", "required": false, "schema": { @@ -33339,7 +40773,7 @@ "schema": { "type": "object", "properties": { - "ID": { + "id": { "type": "string" } } @@ -33349,11 +40783,11 @@ } } }, - "/api/RemoveJITAdminTemplate": { + "/api/RemoveSensitiveInfoType": { "post": { - "summary": "RemoveJITAdminTemplate", + "summary": "RemoveSensitiveInfoType", "tags": [ - "Identity > Administration > Users" + "Security > Compliance-SIT" ], "security": [ { @@ -33372,27 +40806,30 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Identity.Role.ReadWrite", + "x-cipp-role": "Security.SensitiveInfoType.ReadWrite", "parameters": [ { - "name": "ID", + "name": "Identity", "in": "query", "required": false, "schema": { "type": "string" } + }, + { + "$ref": "#/components/parameters/tenantFilter" } ], "requestBody": { @@ -33402,21 +40839,30 @@ "schema": { "type": "object", "properties": { - "ID": { + "Identity": { + "type": "string" + }, + "Name": { + "type": "string" + }, + "tenantFilter": { "type": "string" } - } + }, + "required": [ + "tenantFilter" + ] } } } } } }, - "/api/RemovePolicy": { + "/api/RemoveSensitiveInfoTypeTemplate": { "post": { - "summary": "RemovePolicy", + "summary": "RemoveSensitiveInfoTypeTemplate", "tags": [ - "Endpoint > MEM" + "Security > Compliance-SIT" ], "security": [ { @@ -33435,19 +40881,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Endpoint.MEM.ReadWrite", + "x-cipp-role": "Security.SensitiveInfoType.ReadWrite", "parameters": [ { "name": "ID", @@ -33456,17 +40902,6 @@ "schema": { "type": "string" } - }, - { - "$ref": "#/components/parameters/tenantFilter" - }, - { - "name": "URLName", - "in": "query", - "required": false, - "schema": { - "type": "string" - } } ], "requestBody": { @@ -33478,28 +40913,19 @@ "properties": { "ID": { "type": "string" - }, - "tenantFilter": { - "type": "string" - }, - "URLName": { - "type": "string" } - }, - "required": [ - "tenantFilter" - ] + } } } } } } }, - "/api/RemoveQuarantinePolicy": { + "/api/RemoveSensitivityLabel": { "post": { - "summary": "RemoveQuarantinePolicy", + "summary": "RemoveSensitivityLabel", "tags": [ - "Email-Exchange > Spamfilter" + "Security > Compliance-SensitivityLabel" ], "security": [ { @@ -33518,19 +40944,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Exchange.Spamfilter.ReadWrite", + "x-cipp-role": "Security.SensitivityLabel.ReadWrite", "parameters": [ { "name": "Identity", @@ -33540,14 +40966,6 @@ "type": "string" } }, - { - "name": "Name", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, { "$ref": "#/components/parameters/tenantFilter" } @@ -33565,12 +40983,12 @@ "Name": { "type": "string" }, - "TenantFilter": { + "tenantFilter": { "type": "string" } }, "required": [ - "TenantFilter" + "tenantFilter" ] } } @@ -33578,11 +40996,11 @@ } } }, - "/api/RemoveQueuedAlert": { + "/api/RemoveSensitivityLabelTemplate": { "post": { - "summary": "RemoveQueuedAlert", + "summary": "RemoveSensitivityLabelTemplate", "tags": [ - "Tenant > Administration > Alerts" + "Security > Compliance-SensitivityLabel" ], "security": [ { @@ -33601,28 +41019,20 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "CIPP.Alert.ReadWrite", + "x-cipp-role": "Security.SensitivityLabel.ReadWrite", "parameters": [ - { - "name": "EventType", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, { "name": "ID", "in": "query", @@ -33639,9 +41049,6 @@ "schema": { "type": "object", "properties": { - "EventType": { - "type": "string" - }, "ID": { "type": "string" } @@ -33652,11 +41059,11 @@ } } }, - "/api/RemoveQueuedApp": { + "/api/RemoveSpamfilter": { "post": { - "summary": "RemoveQueuedApp", + "summary": "RemoveSpamfilter", "tags": [ - "Endpoint > Applications" + "Email-Exchange > Spamfilter" ], "security": [ { @@ -33675,19 +41082,32 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Endpoint.Application.ReadWrite", + "x-cipp-role": "Exchange.Spamfilter.ReadWrite", + "parameters": [ + { + "name": "name", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ], "requestBody": { "required": true, "content": { @@ -33695,7 +41115,7 @@ "schema": { "type": "object", "properties": { - "ID": { + "name": { "type": "string" } } @@ -33705,11 +41125,11 @@ } } }, - "/api/RemoveSafeLinksPolicyTemplate": { + "/api/RemoveSpamfilterTemplate": { "post": { - "summary": "RemoveSafeLinksPolicyTemplate", + "summary": "RemoveSpamfilterTemplate", "tags": [ - "Security > Safe-Links-Policy" + "Email-Exchange > Spamfilter" ], "security": [ { @@ -33728,29 +41148,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Exchange.SafeLinks.ReadWrite", - "parameters": [ - { - "name": "ID", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - } - ], + "x-cipp-role": "Exchange.Spamfilter.ReadWrite", "requestBody": { "required": true, "content": { @@ -33768,11 +41178,11 @@ } } }, - "/api/RemoveScheduledItem": { - "post": { - "summary": "RemoveScheduledItem", + "/api/RemoveStandard": { + "get": { + "summary": "RemoveStandard", "tags": [ - "CIPP > Scheduler" + "Tenant > Standards" ], "security": [ { @@ -33791,52 +41201,36 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "description": "Removes a scheduled item from CIPP's scheduler.\n #>\n [CmdletBinding()]\n param($Request, $TriggerMetadata)", - "x-cipp-role": "CIPP.Scheduler.ReadWrite", + "x-cipp-role": "Tenant.Standards.ReadWrite", "parameters": [ { - "name": "id", + "name": "ID", "in": "query", "required": false, "schema": { "type": "string" } } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "id": { - "type": "string" - } - } - } - } - } - } + ] } }, - "/api/RemoveSpamfilter": { + "/api/RemoveStandardTemplate": { "post": { - "summary": "RemoveSpamfilter", + "summary": "RemoveStandardTemplate", "tags": [ - "Email-Exchange > Spamfilter" + "Tenant > Standards" ], "security": [ { @@ -33855,30 +41249,27 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Exchange.Spamfilter.ReadWrite", + "x-cipp-role": "Tenant.Standards.ReadWrite", "parameters": [ { - "name": "name", + "name": "ID", "in": "query", "required": false, "schema": { "type": "string" } - }, - { - "$ref": "#/components/parameters/tenantFilter" } ], "requestBody": { @@ -33888,7 +41279,7 @@ "schema": { "type": "object", "properties": { - "name": { + "ID": { "type": "string" } } @@ -33898,11 +41289,11 @@ } } }, - "/api/RemoveSpamfilterTemplate": { + "/api/RemoveTenantAllowBlockList": { "post": { - "summary": "RemoveSpamfilterTemplate", + "summary": "RemoveTenantAllowBlockList", "tags": [ - "Email-Exchange > Spamfilter" + "Uncategorized" ], "security": [ { @@ -33921,19 +41312,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Exchange.Spamfilter.ReadWrite", + "x-cipp-role": "Exchange.SpamFilter.ReadWrite", "requestBody": { "required": true, "content": { @@ -33941,69 +41332,30 @@ "schema": { "type": "object", "properties": { - "ID": { + "Entries": { + "type": "string" + }, + "ListType": { + "type": "string" + }, + "tenantFilter": { "type": "string" } - } + }, + "required": [ + "tenantFilter" + ] } } } } } }, - "/api/RemoveStandard": { - "get": { - "summary": "RemoveStandard", - "tags": [ - "Tenant > Standards" - ], - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StandardResults" - } - } - } - }, - "400": { - "description": "Bad request — missing required field or invalid input" - }, - "401": { - "description": "Unauthorized — invalid or missing bearer token" - }, - "403": { - "description": "Forbidden — caller lacks the required RBAC role" - }, - "500": { - "description": "Internal server error" - } - }, - "x-cipp-role": "Tenant.Standards.ReadWrite", - "parameters": [ - { - "name": "ID", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/RemoveStandardTemplate": { + "/api/RemoveTenantAllowBlockListTemplate": { "post": { - "summary": "RemoveStandardTemplate", + "summary": "RemoveTenantAllowBlockListTemplate", "tags": [ - "Tenant > Standards" + "Email-Exchange > Spamfilter" ], "security": [ { @@ -34022,19 +41374,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, - "x-cipp-role": "Tenant.Standards.ReadWrite", + "x-cipp-role": "Exchange.SpamFilter.ReadWrite", "parameters": [ { "name": "ID", @@ -34062,68 +41414,6 @@ } } }, - "/api/RemoveTenantAllowBlockList": { - "post": { - "summary": "RemoveTenantAllowBlockList", - "tags": [ - "Uncategorized" - ], - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StandardResults" - } - } - } - }, - "400": { - "description": "Bad request — missing required field or invalid input" - }, - "401": { - "description": "Unauthorized — invalid or missing bearer token" - }, - "403": { - "description": "Forbidden — caller lacks the required RBAC role" - }, - "500": { - "description": "Internal server error" - } - }, - "x-cipp-role": "Exchange.SpamFilter.ReadWrite", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "Entries": { - "type": "string" - }, - "ListType": { - "type": "string" - }, - "tenantFilter": { - "type": "string" - } - }, - "required": [ - "tenantFilter" - ] - } - } - } - } - } - }, "/api/RemoveTenantCapabilitiesCache": { "get": { "summary": "RemoveTenantCapabilitiesCache", @@ -34147,13 +41437,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -34195,13 +41485,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -34267,13 +41557,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -34330,13 +41620,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -34395,13 +41685,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -34478,13 +41768,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -34518,6 +41808,57 @@ } } }, + "/api/RemoveWebhookAlert": { + "get": { + "summary": "RemoveWebhookAlert", + "tags": [ + "Uncategorized" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "CIPP.Alert.ReadWrite", + "parameters": [ + { + "name": "ID", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, "/api/SetAuthMethod": { "post": { "summary": "SetAuthMethod", @@ -34541,13 +41882,13 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" @@ -34583,9 +41924,91 @@ } } }, + "/api/invoke-DomainAnalyser_List": { + "get": { + "summary": "invoke-DomainAnalyser_List", + "tags": [ + "Tenant > Standards" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "x-cipp-role": "Tenant.DomainAnalyser.Read", + "parameters": [ + { + "$ref": "#/components/parameters/tenantFilter" + } + ] + } + }, + "/api/invoke-ListEmptyResults": { + "get": { + "summary": "- Purposely lists an empty result", + "tags": [ + "CIPP > Core" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StandardResults" + } + } + } + }, + "400": { + "description": "Bad request \u2014 missing required field or invalid input" + }, + "401": { + "description": "Unauthorized \u2014 invalid or missing bearer token" + }, + "403": { + "description": "Forbidden \u2014 caller lacks the required RBAC role" + }, + "500": { + "description": "Internal server error" + } + }, + "description": "Returns an empty results array. Used as a placeholder endpoint.", + "x-cipp-role": "CIPP.Core.Read" + } + }, "/api/listStandardTemplates": { "get": { - "summary": "listStandardTemplates", + "summary": "Lists saved standards templates that define sets of standards to apply to tenants.", "tags": [ "Tenant > Standards" ], @@ -34606,18 +42029,19 @@ } }, "400": { - "description": "Bad request — missing required field or invalid input" + "description": "Bad request \u2014 missing required field or invalid input" }, "401": { - "description": "Unauthorized — invalid or missing bearer token" + "description": "Unauthorized \u2014 invalid or missing bearer token" }, "403": { - "description": "Forbidden — caller lacks the required RBAC role" + "description": "Forbidden \u2014 caller lacks the required RBAC role" }, "500": { "description": "Internal server error" } }, + "description": "Lists saved standards templates that define sets of standards to apply to tenants.", "x-cipp-role": "Tenant.Standards.Read", "parameters": [ { From 65ba7a7880657dd81d745ba677a7c57f27f7ee50 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Sun, 7 Jun 2026 13:24:52 +0800 Subject: [PATCH 159/202] embed module into pipeline actions --- Tools/Build-DevApiModules.ps1 | 3 +- Tools/ModuleBuilder/3.1.8/ModuleBuilder.psd1 | 54 + Tools/ModuleBuilder/3.1.8/ModuleBuilder.psm1 | 1390 +++++++++++++++++ Tools/ModuleBuilder/3.1.8/PSGetModuleInfo.xml | 165 ++ .../3.1.8/en-US/about_ModuleBuilder.help.txt | 88 ++ 5 files changed, 1698 insertions(+), 2 deletions(-) create mode 100644 Tools/ModuleBuilder/3.1.8/ModuleBuilder.psd1 create mode 100644 Tools/ModuleBuilder/3.1.8/ModuleBuilder.psm1 create mode 100644 Tools/ModuleBuilder/3.1.8/PSGetModuleInfo.xml create mode 100644 Tools/ModuleBuilder/3.1.8/en-US/about_ModuleBuilder.help.txt diff --git a/Tools/Build-DevApiModules.ps1 b/Tools/Build-DevApiModules.ps1 index 04b94fbe195c..f8bcfcac166d 100644 --- a/Tools/Build-DevApiModules.ps1 +++ b/Tools/Build-DevApiModules.ps1 @@ -5,8 +5,7 @@ $repoRoot = Split-Path -Parent $toolsRoot $modulesRoot = Join-Path $repoRoot 'Modules' $outputRoot = Join-Path $repoRoot 'Output' -Install-Module -Name ModuleBuilder -MaximumVersion 3.1.9 -Scope CurrentUser -Force -AllowClobber -Import-Module -Name ModuleBuilder -Force +Import-Module -Name (Join-Path $toolsRoot 'ModuleBuilder\3.1.8\ModuleBuilder.psd1') -Force Write-Host "Repo root: $repoRoot" Set-Location -Path $repoRoot diff --git a/Tools/ModuleBuilder/3.1.8/ModuleBuilder.psd1 b/Tools/ModuleBuilder/3.1.8/ModuleBuilder.psd1 new file mode 100644 index 000000000000..91c3200bd7bd --- /dev/null +++ b/Tools/ModuleBuilder/3.1.8/ModuleBuilder.psd1 @@ -0,0 +1,54 @@ +@{ + # The module version should be SemVer.org compatible + ModuleVersion = '3.1.8' + + # PrivateData is where all third-party metadata goes + PrivateData = @{ + # PrivateData.PSData is the PowerShell Gallery data + PSData = @{ + # Prerelease string should be here, so we can set it + Prerelease = '' + + # Release Notes have to be here, so we can update them + ReleaseNotes = ' + ModuleBuilder v3.1.8+Build.local.Branch.main.Sha.b4d5aaf9df98194aa7d40c46cd3d7ca787011c54.Date.20250412T215606 + Fix case sensitivity of defaults for SourceDirectories and PublicFilter + ' + + # Tags applied to this module. These help with module discovery in online galleries. + Tags = 'Authoring','Build','Development','BestPractices' + + # A URL to the license for this module. + LicenseUri = 'https://github.com/PoshCode/ModuleBuilder/blob/master/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/PoshCode/ModuleBuilder' + + # A URL to an icon representing this module. + IconUri = 'https://github.com/PoshCode/ModuleBuilder/blob/resources/ModuleBuilder.png?raw=true' + } # End of PSData + } # End of PrivateData + + # The main script module that is automatically loaded as part of this module + RootModule = 'ModuleBuilder.psm1' + + # Modules that must be imported into the global environment prior to importing this module + RequiredModules = @('Configuration') + + # Always define FunctionsToExport as an empty @() which will be replaced on build + FunctionsToExport = @('Build-Module','Convert-Breakpoint','Convert-CodeCoverage','ConvertFrom-SourceLineNumber','ConvertTo-SourceLineNumber') + AliasesToExport = @('build','Convert-LineNumber') + + # ID used to uniquely identify this module + GUID = '4775ad56-8f64-432f-8da7-87ddf7a34653' + Description = 'A module for authoring and building PowerShell modules' + + # Common stuff for all our modules: + CompanyName = 'PoshCode' + Author = 'Joel Bennett' + Copyright = "Copyright 2018 Joel Bennett" + + # Minimum version of the Windows PowerShell engine required by this module + PowerShellVersion = '5.1' + CompatiblePSEditions = @('Core','Desktop') +} diff --git a/Tools/ModuleBuilder/3.1.8/ModuleBuilder.psm1 b/Tools/ModuleBuilder/3.1.8/ModuleBuilder.psm1 new file mode 100644 index 000000000000..70e6bb32d588 --- /dev/null +++ b/Tools/ModuleBuilder/3.1.8/ModuleBuilder.psm1 @@ -0,0 +1,1390 @@ +#Region './Classes/AliasVisitor.ps1' -1 + +using namespace System.Management.Automation.Language +using namespace System.Collections.Generic + +# This is used only to parse the parameters to New|Set|Remove-Alias +# NOTE: this is _part of_ the implementation of AliasVisitor, but ... +# PowerShell can't handle nested classes so I left it outside, +# but I kept it here in this file. +class AliasParameterVisitor : AstVisitor { + [string]$Parameter = $null + [string]$Command = $null + [string]$Name = $null + [string]$Value = $null + [string]$Scope = $null + + # Parameter Names + [AstVisitAction] VisitCommandParameter([CommandParameterAst]$ast) { + $this.Parameter = $ast.ParameterName + return [AstVisitAction]::Continue + } + + # Parameter Values + [AstVisitAction] VisitStringConstantExpression([StringConstantExpressionAst]$ast) { + # The FIRST command element is always the command name + if (!$this.Command) { + $this.Command = $ast.Value + return [AstVisitAction]::Continue + } else { + # Nobody should use minimal parameters like -N for -Name ... + # But if they do, our parser works anyway! + switch -Wildcard ($this.Parameter) { + "S*" { + $this.Scope = $ast.Value + } + "N*" { + $this.Name = $ast.Value + } + "Va*" { + $this.Value = $ast.Value + } + "F*" { + if ($ast.Value) { + # Force parameter was passed as named parameter with a positional parameter after it which is alias name + $this.Name = $ast.Value + } + } + default { + if (!$this.Parameter) { + # For bare arguments, the order is Name, Value: + if (!$this.Name) { + $this.Name = $ast.Value + } else { + $this.Value = $ast.Value + } + } + } + } + + $this.Parameter = $null + + # If we have enough information, stop the visit + # For -Scope global or Remove-Alias, we don't want to export these + if ($this.Name -and $this.Command -eq "Remove-Alias") { + $this.Command = "Remove-Alias" + return [AstVisitAction]::StopVisit + } elseif ($this.Name -and $this.Scope -eq "Global") { + return [AstVisitAction]::StopVisit + } + return [AstVisitAction]::Continue + } + } + + [AliasParameterVisitor] Clear() { + $this.Command = $null + $this.Parameter = $null + $this.Name = $null + $this.Value = $null + $this.Scope = $null + return $this + } +} + +# This visits everything at the top level of the script +class AliasVisitor : AstVisitor { + [HashSet[String]]$Aliases = @() + [AliasParameterVisitor]$Parameters = @{} + + # The [Alias(...)] attribute on functions matters, but we can't export aliases that are defined inside a function + [AstVisitAction] VisitFunctionDefinition([FunctionDefinitionAst]$ast) { + @($ast.Body.ParamBlock.Attributes.Where{ + $_.TypeName.Name -eq "Alias" + }.PositionalArguments.Value).ForEach{ + if ($_) { + $this.Aliases.Add($_) + } + } + + return [AstVisitAction]::SkipChildren + } + + # Top-level commands matter, but only if they're alias commands + [AstVisitAction] VisitCommand([CommandAst]$ast) { + if ($ast.CommandElements[0].Value -imatch "(New|Set|Remove)-Alias") { + $ast.Visit($this.Parameters.Clear()) + + # We COULD just remove it (even if we didn't add it) ... + if ($this.Parameters.Command -ieq "Remove-Alias") { + # But Write-Verbose for logging purposes + if ($this.Aliases.Contains($this.Parameters.Name)) { + Write-Verbose -Message "Alias '$($this.Parameters.Name)' is removed by line $($ast.Extent.StartLineNumber): $($ast.Extent.Text)" + $this.Aliases.Remove($this.Parameters.Name) + } + # We don't need to export global aliases, because they broke out already + } elseif ($this.Parameters.Name -and $this.Parameters.Scope -ine 'Global') { + $this.Aliases.Add($this.Parameters.Name) + } + } + return [AstVisitAction]::SkipChildren + } +} +#EndRegion './Classes/AliasVisitor.ps1' 120 +#Region './Private/ConvertToAst.ps1' -1 + +function ConvertToAst { + <# + .SYNOPSIS + Parses the given code and returns an object with the AST, Tokens and ParseErrors + #> + param( + # The script content, or script or module file path to parse + [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] + [Alias("Path", "PSPath", "Definition", "ScriptBlock", "Module")] + $Code + ) + process { + Write-Debug " ENTER: ConvertToAst $Code" + $ParseErrors = $null + $Tokens = $null + if ($Code | Test-Path -ErrorAction SilentlyContinue) { + Write-Debug " Parse Code as Path" + $AST = [System.Management.Automation.Language.Parser]::ParseFile(($Code | Convert-Path), [ref]$Tokens, [ref]$ParseErrors) + } elseif ($Code -is [System.Management.Automation.FunctionInfo]) { + Write-Debug " Parse Code as Function" + $String = "function $($Code.Name) { $($Code.Definition) }" + $AST = [System.Management.Automation.Language.Parser]::ParseInput($String, [ref]$Tokens, [ref]$ParseErrors) + } else { + Write-Debug " Parse Code as String" + $AST = [System.Management.Automation.Language.Parser]::ParseInput([String]$Code, [ref]$Tokens, [ref]$ParseErrors) + } + + Write-Debug " EXIT: ConvertToAst" + [PSCustomObject]@{ + PSTypeName = "PoshCode.ModuleBuilder.ParseResults" + ParseErrors = $ParseErrors + Tokens = $Tokens + AST = $AST + } + } +} +#EndRegion './Private/ConvertToAst.ps1' 37 +#Region './Private/CopyReadMe.ps1' -1 + +function CopyReadMe { + [CmdletBinding()] + param( + # The path to the ReadMe document to copy + [Parameter(ValueFromPipelineByPropertyName)] + [AllowNull()][AllowEmptyString()] + [string]$ReadMe, + + # The name of the module -- because the file is renamed to about_$ModuleName.help.txt + [Parameter(Mandatory,ValueFromPipelineByPropertyName)] + [Alias("Name")] + [string]$ModuleName, + + [Parameter(Mandatory,ValueFromPipelineByPropertyName)] + [string]$OutputDirectory, + + # The culture (language) to store the ReadMe as (defaults to "en") + [Parameter(ValueFromPipelineByPropertyName)] + [Globalization.CultureInfo]$Culture = $(Get-UICulture), + + # If set, overwrite the existing readme + [Switch]$Force + ) + process { + # Copy the readme file as an about_ help file + Write-Verbose "Test for ReadMe: $Pwd/$($ReadMe)" + if ($ReadMe -and (Test-Path $ReadMe -PathType Leaf)) { + # Make sure there's a language path + $LanguagePath = Join-Path $OutputDirectory $Culture + if (!(Test-Path $LanguagePath -PathType Container)) { + $null = New-Item $LanguagePath -Type Directory -Force + } + Write-Verbose "Copy ReadMe to: $LanguagePath" + + $about_module = Join-Path $LanguagePath "about_$($ModuleName).help.txt" + if (!(Test-Path $about_module)) { + Write-Verbose "Turn readme into about_module" + Copy-Item -LiteralPath $ReadMe -Destination $about_module -Force:$Force + } + } + } +} +#EndRegion './Private/CopyReadMe.ps1' 43 +#Region './Private/GetBuildInfo.ps1' -1 + +function GetBuildInfo { + [CmdletBinding()] + param( + # The path to the Build Manifest Build.psd1 + [Parameter()] + [AllowNull()] + [string]$BuildManifest, + + # Pass MyInvocation from the Build-Command so we can read parameter values + [Parameter(DontShow)] + [AllowNull()] + $BuildCommandInvocation + ) + + $BuildInfo = if ($BuildManifest -and (Test-Path $BuildManifest) -and (Split-path -Leaf $BuildManifest) -eq 'build.psd1') { + # Read the build.psd1 configuration file for default parameter values + Write-Debug "Load Build Manifest $BuildManifest" + Import-Metadata -Path $BuildManifest + } else { + @{} + } + + $CommonParameters = [System.Management.Automation.Cmdlet]::CommonParameters + + [System.Management.Automation.Cmdlet]::OptionalCommonParameters + $BuildParameters = $BuildCommandInvocation.MyCommand.Parameters + # Make we can always look things up in BoundParameters + $BoundParameters = if ($BuildCommandInvocation.BoundParameters) { + $BuildCommandInvocation.BoundParameters + } else { + @{} + } + + # Combine the defaults with parameter values + $ParameterValues = @{} + if ($BuildCommandInvocation) { + foreach ($parameter in $BuildParameters.GetEnumerator().Where({$_.Key -notin $CommonParameters})) { + Write-Debug " Parameter: $($parameter.key)" + $key = $parameter.Key + + # We want to map the parameter aliases to the parameter name: + foreach ($k in @($parameter.Value.Aliases)) { + if ($null -ne $k -and $BuildInfo.ContainsKey($k)) { + Write-Debug " ... Update BuildInfo[$key] from $k" + $BuildInfo[$key] = $BuildInfo[$k] + $null = $BuildInfo.Remove($k) + } + } + # Bound parameter values > build.psd1 values > default parameters values + if (-not $BuildInfo.ContainsKey($key) -or $BoundParameters.ContainsKey($key)) { + # Reading the current value of the $key variable returns either the bound parameter or the default + if ($null -ne ($value = Get-Variable -Name $key -ValueOnly -ErrorAction Ignore )) { + if ($value -ne ($null -as $parameter.Value.ParameterType)) { + $ParameterValues[$key] = $value + } + } + if ($BoundParameters.ContainsKey($key)) { + Write-Debug " From Parameter: $($ParameterValues[$key] -join ', ')" + } elseif ($ParameterValues[$key]) { + Write-Debug " From Default: $($ParameterValues[$key] -join ', ')" + } + } elseif ($BuildInfo[$key]) { + Write-Debug " From Manifest: $($BuildInfo[$key] -join ', ')" + } + } + } + # BuildInfo.SourcePath should point to a module manifest + if ($BuildInfo.SourcePath -and $BuildInfo.SourcePath -ne $BuildManifest) { + Write-Debug " Updating: SourcePath" + Write-Debug " To: $($BuildInfo.SourcePath)" + $ParameterValues["SourcePath"] = $BuildInfo.SourcePath + } + # If SourcePath point to build.psd1, we should clear it + if ($ParameterValues["SourcePath"] -eq $BuildManifest) { + Write-Debug " Removing: SourcePath" + $ParameterValues.Remove("SourcePath") + } + Write-Debug "Finished parsing Build Manifest $BuildManifest" + + $BuildManifestParent = if ($BuildManifest) { + Split-Path -Parent $BuildManifest + } else { + Get-Location -PSProvider FileSystem + } + + if ((-not $BuildInfo.SourcePath) -and $ParameterValues["SourcePath"] -notmatch '\.psd1') { + Write-Debug " Searching: SourcePath ($BuildManifestParent/**/*.psd1)" + # Find a module manifest (or maybe several) + $ModuleInfo = Get-ChildItem $BuildManifestParent -Recurse -Filter *.psd1 -ErrorAction SilentlyContinue | + ImportModuleManifest -ErrorAction SilentlyContinue + # If we found more than one module info, the only way we have of picking just one is if it matches a folder name + if (@($ModuleInfo).Count -gt 1) { + Write-Debug (@(@(" Found $(@($ModuleInfo).Count):") + @($ModuleInfo.Path)) -join "`n ") + # It can't be a module that needs building unless it has either: + $ModuleInfo = $ModuleInfo.Where{ + $Root = Split-Path $_.Path + @( + # - A build.psd1 next to it + Test-Path (Join-Path $Root "build.ps1") -PathType Leaf + # - A Public (or Private) folder with source scripts in it + Test-Path (Join-Path $Root "Public") -PathType Container + Test-Path (Join-Path $Root "Private") -PathType Container + ) -contains $true + } + Write-Debug (@(@(" Filtered $(@($ModuleInfo).Count):") + @($ModuleInfo.Path)) -join "`n ") + } + if (@($ModuleInfo).Count -eq 1) { + Write-Debug "Updating BuildInfo SourcePath to $($ModuleInfo.Path)" + $ParameterValues["SourcePath"] = $ModuleInfo.Path + } else { + throw "Can't determine the module manifest in $BuildManifestParent" + } + } + + $BuildInfo = $BuildInfo | Update-Object $ParameterValues + Write-Debug "Using Module Manifest $($BuildInfo.SourcePath)" + + # Make sure the SourcePath is absolute and points at an actual file + if (!(Split-Path -IsAbsolute $BuildInfo.SourcePath) -and $BuildManifestParent) { + $BuildInfo.SourcePath = Join-Path $BuildManifestParent $BuildInfo.SourcePath | Convert-Path + } else { + $BuildInfo.SourcePath = Convert-Path $BuildInfo.SourcePath + } + if (!(Test-Path $BuildInfo.SourcePath)) { + throw "Can't find module manifest at the specified SourcePath: $($BuildInfo.SourcePath)" + } + + $BuildInfo +} +#EndRegion './Private/GetBuildInfo.ps1' 129 +#Region './Private/GetCommandAlias.ps1' -1 + + +function GetCommandAlias { + <# + .SYNOPSIS + Parses one or more files for aliases and returns a list of alias names. + #> + [CmdletBinding()] + [OutputType([System.Collections.Generic.Hashset[string]])] + param( + # The AST to find aliases in + [Parameter(Mandatory, ValueFromPipelineByPropertyName, ValueFromPipeline)] + [System.Management.Automation.Language.Ast]$Ast + ) + begin { + $Visitor = [AliasVisitor]::new() + } + process { + $Ast.Visit($Visitor) + } + end { + $Visitor.Aliases + } +} + +#EndRegion './Private/GetCommandAlias.ps1' 25 +#Region './Private/GetRelativePath.ps1' -1 + +function GetRelativePath { + <# + .SYNOPSIS + Returns the relative path, or $Path if the paths don't share the same root. + For backward compatibility, this is [System.IO.Path]::GetRelativePath for .NET 4.x + #> + [OutputType([string])] + [CmdletBinding()] + param( + # The source path the result should be relative to. This path is always considered to be a directory. + [Parameter(Mandatory)] + [string]$RelativeTo, + + # The destination path. + [Parameter(Mandatory)] + [string]$Path + ) + + # This giant mess is because PowerShell drives aren't valid filesystem drives + $Drive = $Path -replace "^([^\\/]+:[\\/])?.*", '$1' + if ($Drive -ne ($RelativeTo -replace "^([^\\/]+:[\\/])?.*", '$1')) { + Write-Verbose "Paths on different drives" + return $Path # no commonality, different drive letters on windows + } + $RelativeTo = $RelativeTo -replace "^[^\\/]+:[\\/]", [IO.Path]::DirectorySeparatorChar + $Path = $Path -replace "^[^\\/]+:[\\/]", [IO.Path]::DirectorySeparatorChar + $RelativeTo = [IO.Path]::GetFullPath($RelativeTo).TrimEnd('\/') -replace "^[^\\/]+:[\\/]", [IO.Path]::DirectorySeparatorChar + $Path = [IO.Path]::GetFullPath($Path) -replace "^[^\\/]+:[\\/]", [IO.Path]::DirectorySeparatorChar + + $commonLength = 0 + while ($Path[$commonLength] -eq $RelativeTo[$commonLength]) { + $commonLength++ + } + if ($commonLength -eq $RelativeTo.Length -and $RelativeTo.Length -eq $Path.Length) { + Write-Verbose "Equal Paths" + return "." # The same paths + } + if ($commonLength -eq 0) { + Write-Verbose "Paths on different drives?" + return $Drive + $Path # no commonality, different drive letters on windows + } + + Write-Verbose "Common base: $commonLength $($RelativeTo.Substring(0,$commonLength))" + # In case we matched PART of a name, like C:\Users\Joel and C:\Users\Joe + while ($commonLength -gt $RelativeTo.Length -and ($RelativeTo[$commonLength] -ne [IO.Path]::DirectorySeparatorChar)) { + $commonLength-- + } + + Write-Verbose "Common base: $commonLength $($RelativeTo.Substring(0,$commonLength))" + # create '..' segments for segments past the common on the "$RelativeTo" path + if ($commonLength -lt $RelativeTo.Length) { + $result = @('..') * @($RelativeTo.Substring($commonLength).Split([IO.Path]::DirectorySeparatorChar).Where{ $_ }).Length -join ([IO.Path]::DirectorySeparatorChar) + } + (@($result, $Path.Substring($commonLength).TrimStart([IO.Path]::DirectorySeparatorChar)).Where{ $_ } -join ([IO.Path]::DirectorySeparatorChar)) +} +#EndRegion './Private/GetRelativePath.ps1' 56 +#Region './Private/ImportModuleManifest.ps1' -1 + +function ImportModuleManifest { + [CmdletBinding()] + param( + [Alias("PSPath")] + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] + [string]$Path + ) + process { + # Get all the information in the module manifest + $ModuleInfo = Get-Module $Path -ListAvailable -WarningAction SilentlyContinue -ErrorAction SilentlyContinue -ErrorVariable Problems + + # Some versions fails silently. If the GUID is empty, we didn't get anything at all + if ($ModuleInfo.Guid -eq [Guid]::Empty) { + Write-Error "Cannot parse '$Path' as a module manifest, try Test-ModuleManifest for details" + return + } + + # Some versions show errors are when the psm1 doesn't exist (yet), but we don't care + $ErrorsWeIgnore = "^" + (@( + "Modules_InvalidRequiredModulesinModuleManifest" + "Modules_InvalidRootModuleInModuleManifest" + ) -join "|^") + + # If there are any OTHER problems we'll fail + if ($Problems = $Problems.Where({ $_.FullyQualifiedErrorId -notmatch $ErrorsWeIgnore })) { + foreach ($problem in $Problems) { + Write-Error $problem + } + # Short circuit - don't output the ModuleInfo if there were errors + return + } + + # Workaround the fact that Get-Module returns the DefaultCommandPrefix as Prefix + Update-Object -InputObject $ModuleInfo -UpdateObject @{ DefaultCommandPrefix = $ModuleInfo.Prefix; Prefix = "" } + } +} +#EndRegion './Private/ImportModuleManifest.ps1' 37 +#Region './Private/InitializeBuild.ps1' -1 + +function InitializeBuild { + <# + .SYNOPSIS + Loads build.psd1 and the module manifest and combines them with the parameter values of the calling function. + .DESCRIPTION + This function is for internal use from Build-Module only + It does a few things that make it really only work properly there: + + 1. It calls ResolveBuildManifest to resolve the Build.psd1 from the given -SourcePath (can be Folder, Build.psd1 or Module manifest path) + 2. Then calls GetBuildInfo to read the Build configuration file and override parameters passed through $Invocation (read from the PARENT MyInvocation) + 2. It gets the Module information from the ModuleManifest, and merges it with the $ModuleInfo + .NOTES + Depends on the Configuration module Update-Object and (the built in Import-LocalizedData and Get-Module) + #> + [CmdletBinding()] + param( + # The root folder where the module source is (including the Build.psd1 and the module Manifest.psd1) + [string]$SourcePath, + + [Parameter(DontShow)] + [AllowNull()] + $BuildCommandInvocation = $(Get-Variable MyInvocation -Scope 1 -ValueOnly) + ) + Write-Debug "Initializing build variables" + + # GetBuildInfo reads the parameter values from the Build-Module command and combines them with the Manifest values + $BuildManifest = ResolveBuildManifest $SourcePath + + Write-Debug "BuildCommand: $( + @( + @($BuildCommandInvocation.MyCommand.Name) + @($BuildCommandInvocation.BoundParameters.GetEnumerator().ForEach{ "-{0} '{1}'" -f $_.Key, $_.Value }) + ) -join ' ')" + $BuildInfo = GetBuildInfo -BuildManifest $BuildManifest -BuildCommandInvocation $BuildCommandInvocation + + # Normalize the version (if it was passed in via build.psd1) + if ($BuildInfo.SemVer) { + Write-Verbose "Update the Version, Prerelease, and BuildMetadata from the SemVer (in case it was passed in via build.psd1)" + $BuildInfo = $BuildInfo | Update-Object @{ + Prerelease = $BuildInfo.SemVer.Split("+")[0].Split("-", 2)[1] + BuildMetadata = $BuildInfo.SemVer.Split("+", 2)[1] + Version = if (($V = $BuildInfo.SemVer.Split("+")[0].Split("-", 2)[0])) { + [version]$V + } + } + } elseif($BuildInfo.Version) { + Write-Verbose "Calculate the Semantic Version from the Version - Prerelease + BuildMetadata" + $SemVer = "$($BuildInfo.Version)" + if ($BuildInfo.Prerelease) { + $SemVer = "$SemVer-$($BuildInfo.Prerelease)" + } + if ($BuildInfo.BuildMetadata) { + $SemVer = "$SemVer+$($BuildInfo.BuildMetadata)" + } + $BuildInfo = $BuildInfo | Update-Object @{ SemVer = $SemVer } + } + + # Override VersionedOutputDirectory with UnversionedOutputDirectory + if ($BuildInfo.UnversionedOutputDirectory -and $BuildInfo.VersionedOutputDirectory) { + $BuildInfo.VersionedOutputDirectory = $false + } + + # Finally, add all the information in the module manifest to the return object + if ($ModuleInfo = ImportModuleManifest $BuildInfo.SourcePath) { + # Update the module manifest with our build configuration and output it + Update-Object -InputObject $ModuleInfo -UpdateObject $BuildInfo + } else { + throw "Unresolvable problems in module manifest: '$($BuildInfo.SourcePath)'" + } +} +#EndRegion './Private/InitializeBuild.ps1' 71 +#Region './Private/MoveUsingStatements.ps1' -1 + +function MoveUsingStatements { + <# + .SYNOPSIS + A command to comment out and copy to the top of the file the Using Statements + .DESCRIPTION + When all files are merged together, the Using statements from individual files + don't necessarily end up at the beginning of the PSM1, creating Parsing Errors. + + This function uses AST to comment out those statements (to preserver line numbering) + and insert them (conserving order) at the top of the script. + #> + [CmdletBinding()] + param( + # Path to the PSM1 file to amend + [Parameter(Mandatory, ValueFromPipelineByPropertyName, ValueFromPipeline)] + [System.Management.Automation.Language.Ast]$AST, + + [Parameter(ValueFromPipelineByPropertyName, ValueFromPipeline)] + [AllowNull()] + [System.Management.Automation.Language.ParseError[]]$ParseErrors, + + # The encoding defaults to UTF8 (or UTF8NoBom on Core) + [Parameter(DontShow)] + [string]$Encoding = $(if ($IsCoreCLR) { + "UTF8NoBom" + } else { + "UTF8" + }) + ) + process { + # Avoid modifying the file if there's no Parsing Error caused by Using Statements or other errors + if (!$ParseErrors.Where{ $_.ErrorId -eq 'UsingMustBeAtStartOfScript' }) { + Write-Debug "No using statement errors found." + return + } else { + # as decided https://github.com/PoshCode/ModuleBuilder/issues/96 + Write-Debug "Parsing errors found. We'll still attempt to Move using statements." + } + + # Find all Using statements including those non erroring (to conserve their order) + $UsingStatementExtents = $AST.FindAll( + { $Args[0] -is [System.Management.Automation.Language.UsingStatementAst] }, + $false + ).Extent + + # Edit the Script content by commenting out existing statements (conserving line numbering) + $ScriptText = $AST.Extent.Text + $InsertedCharOffset = 0 + $StatementsToCopy = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) + + foreach ($UsingSatement in $UsingStatementExtents) { + $ScriptText = $ScriptText.Insert($UsingSatement.StartOffset + $InsertedCharOffset, '#') + $InsertedCharOffset++ + + # Keep track of unique statements we'll need to insert at the top + $null = $StatementsToCopy.Add($UsingSatement.Text) + } + + $ScriptText = $ScriptText.Insert(0, ($StatementsToCopy -join "`r`n") + "`r`n") + $null = Set-Content -Value $ScriptText -Path $RootModule -Encoding $Encoding + + # Verify we haven't introduced new Parsing errors + $null = [System.Management.Automation.Language.Parser]::ParseFile( + $RootModule, + [ref]$null, + [ref]$ParseErrors + ) + + if ($ParseErrors.Count) { + $Message = $ParseErrors | + Format-Table -Auto @{n = "File"; expr = { $_.Extent.File | Split-Path -Leaf }}, + @{n = "Line"; expr = { $_.Extent.StartLineNumber }}, + Extent, ErrorId, Message | Out-String + Write-Warning "Parse errors in build output:`n$Message" + } + } +} +#EndRegion './Private/MoveUsingStatements.ps1' 78 +#Region './Private/ParameterValues.ps1' -1 + +Update-TypeData -TypeName System.Management.Automation.InvocationInfo -MemberName ParameterValues -MemberType ScriptProperty -Value { + $results = @{} + foreach ($key in $this.MyCommand.Parameters.Keys) { + if ($this.BoundParameters.ContainsKey($key)) { + $results.$key = $this.BoundParameters.$key + } elseif ($value = Get-Variable -Name $key -Scope 1 -ValueOnly -ErrorAction Ignore) { + $results.$key = $value + } + } + return $results +} -Force +#EndRegion './Private/ParameterValues.ps1' 12 +#Region './Private/ParseLineNumber.ps1' -1 + +function ParseLineNumber { + <# + .SYNOPSIS + Parses the SourceFile and SourceLineNumber from a position message + .DESCRIPTION + Parses messages like: + at , : line 1 + at C:\Test\Path\ErrorMaker.ps1:31 char:1 + at C:\Test\Path\Modules\ErrorMaker\ErrorMaker.psm1:27 char:4 + #> + [Cmdletbinding()] + param( + # A position message, starting with "at ..." and containing a line number + [Parameter(ValueFromPipeline)] + [string]$PositionMessage + ) + process { + foreach($line in $PositionMessage -split "\r?\n") { + # At (optional invocation,) :(maybe " line ") number + if ($line -match "at(?: (?[^,]+),)?\s+(?.+):(?\d+)(?: char:(?\d+))?") { + [PSCustomObject]@{ + PSTypeName = "Position" + SourceFile = $matches.SourceFile + SourceLineNumber = $matches.SourceLineNumber + OffsetInLine = $matches.OffsetInLine + PositionMessage = $line + PSScriptRoot = Split-Path $matches.SourceFile + PSCommandPath = $matches.SourceFile + InvocationBlock = $matches.InvocationBlock + } + } elseif($line -notmatch "\s*\+") { + Write-Warning "Can't match: '$line'" + } + } + } +} +#EndRegion './Private/ParseLineNumber.ps1' 37 +#Region './Private/ResolveBuildManifest.ps1' -1 + +function ResolveBuildManifest { + [CmdletBinding()] + param( + # The Source folder path, the Build Manifest Path, or the Module Manifest path used to resolve the Build.psd1 + [Alias("BuildManifest")] + [string]$SourcePath = $(Get-Location -PSProvider FileSystem) + ) + Write-Debug "ResolveBuildManifest $SourcePath" + if ((Split-Path $SourcePath -Leaf) -eq 'build.psd1') { + $BuildManifest = $SourcePath + } elseif (Test-Path $SourcePath -PathType Leaf) { + # When you pass the SourcePath as parameter, you must have the Build Manifest in the same folder + $BuildManifest = Join-Path (Split-Path -Parent $SourcePath) [Bb]uild.psd1 + } else { + # It's a container, assume the Build Manifest is directly under + $BuildManifest = Join-Path $SourcePath [Bb]uild.psd1 + } + + # Make sure we are resolving the absolute path to the manifest, and test it exists + $ResolvedBuildManifest = (Resolve-Path $BuildManifest -ErrorAction SilentlyContinue).Path + + if ($ResolvedBuildManifest) { + $ResolvedBuildManifest + } + +} +#EndRegion './Private/ResolveBuildManifest.ps1' 27 +#Region './Private/ResolveOutputFolder.ps1' -1 + +function ResolveOutputFolder { + [CmdletBinding()] + param( + # The name of the module to build + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] + [Alias("Name")] + [string]$ModuleName, + + # Where to resolve the $OutputDirectory from when relative + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] + [Alias("ModuleBase")] + [string]$Source, + + # Where to build the module. + # Defaults to an \output folder, adjacent to the "SourcePath" folder + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] + [string]$OutputDirectory, + + # specifies the module version for use in the output path if -VersionedOutputDirectory is true + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] + [Alias("ModuleVersion")] + [string]$Version, + + # If set (true) adds a folder named after the version number to the OutputDirectory + [Parameter(ValueFromPipelineByPropertyName)] + [Alias("Force")] + [switch]$VersionedOutputDirectory, + + # Controls whether or not there is a build or cleanup performed + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] + [ValidateSet("Clean", "Build", "CleanBuild")] + [string]$Target = "CleanBuild" + ) + process { + Write-Verbose "Resolve OutputDirectory path: $OutputDirectory" + + # Ensure the OutputDirectory makes sense (it's never blank anymore) + if (!(Split-Path -IsAbsolute $OutputDirectory)) { + # Relative paths are relative to the ModuleBase + $OutputDirectory = Join-Path $Source $OutputDirectory + } + # If they passed in a path with ModuleName\Version on the end... + if ((Split-Path $OutputDirectory -Leaf).EndsWith($Version) -and (Split-Path (Split-Path $OutputDirectory) -Leaf) -eq $ModuleName) { + # strip the version (so we can add it back) + $VersionedOutputDirectory = $true + $OutputDirectory = Split-Path $OutputDirectory + } + # Ensure the OutputDirectory is named "ModuleName" + if ((Split-Path $OutputDirectory -Leaf) -ne $ModuleName) { + # If it wasn't, add a "ModuleName" + $OutputDirectory = Join-Path $OutputDirectory $ModuleName + } + # Ensure the OutputDirectory is not a parent of the SourceDirectory + $RelativeOutputPath = GetRelativePath $OutputDirectory $Source + if (-not $RelativeOutputPath.StartsWith("..") -and $RelativeOutputPath -ne $Source) { + Write-Verbose "Added Version to OutputDirectory path: $OutputDirectory" + $OutputDirectory = Join-Path $OutputDirectory $Version + } + # Ensure the version number is on the OutputDirectory if it's supposed to be + if ($VersionedOutputDirectory -and -not (Split-Path $OutputDirectory -Leaf).EndsWith($Version)) { + Write-Verbose "Added Version to OutputDirectory path: $OutputDirectory" + $OutputDirectory = Join-Path $OutputDirectory $Version + } + + if (Test-Path $OutputDirectory -PathType Leaf) { + throw "Unable to build. There is a file in the way at $OutputDirectory" + } + + if ($Target -match "Clean") { + Write-Verbose "Cleaning $OutputDirectory" + if (Test-Path $OutputDirectory -PathType Container) { + Remove-Item $OutputDirectory -Recurse -Force + } + } + if ($Target -match "Build") { + # Make sure the OutputDirectory exists (relative to ModuleBase or absolute) + New-Item $OutputDirectory -ItemType Directory -Force | Convert-Path + } + } +} +#EndRegion './Private/ResolveOutputFolder.ps1' 81 +#Region './Private/SetModuleContent.ps1' -1 + +function SetModuleContent { + <# + .SYNOPSIS + A wrapper for Set-Content that handles arrays of file paths + .DESCRIPTION + The implementation here is strongly dependent on Build-Module doing the right thing + Build-Module can optionally pass a PREFIX or SUFFIX, but otherwise only passes files + + Because of that, SetModuleContent doesn't test for that + + The goal here is to pretend this is a pipeline, for the sake of memory and file IO + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "OutputPath", Justification = "The rule is buggy")] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "Encoding", Justification = "The rule is buggy ")] + [CmdletBinding()] + param( + # Where to write the joined output + [Parameter(Position=0, Mandatory)] + [string]$OutputPath, + + # Input files, the scripts that will be copied to the output path + # The FIRST and LAST items can be text content instead of file paths. + [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] + [Alias("PSPath", "FullName")] + [AllowEmptyCollection()] + [string[]]$SourceFile, + + # The working directory (allows relative paths for other values) + [string]$WorkingDirectory = $pwd, + + # The encoding defaults to UTF8 (or UTF8NoBom on Core) + [Parameter(DontShow)] + [string]$Encoding = $(if($IsCoreCLR) { "UTF8Bom" } else { "UTF8" }) + ) + begin { + Write-Debug "SetModuleContent WorkingDirectory $WorkingDirectory" + Push-Location $WorkingDirectory -StackName SetModuleContent + $ContentStarted = $false # There has been no content yet + + # Create a proxy command style scriptblock for Set-Content to keep the file handle open + $SetContentCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Management\Set-Content', [System.Management.Automation.CommandTypes]::Cmdlet) + $SetContent = {& $SetContentCmd -Path $OutputPath -Encoding $Encoding}.GetSteppablePipeline($myInvocation.CommandOrigin) + $SetContent.Begin($true) + } + process { + foreach($file in $SourceFile) { + if($SourceName = Resolve-Path $file -Relative -ErrorAction SilentlyContinue) { + Write-Verbose "Adding $SourceName" + # Setting offset to -1 because of the new line we're adding. + # This is needed for the code coverage calculation. + $SetContent.Process("#Region '$SourceName' -1`n") + Get-Content $SourceName -OutVariable source | ForEach-Object { $SetContent.Process($_) } + $SetContent.Process("#EndRegion '$SourceName' $($Source.Count+1)") + } else { + if(!$ContentStarted) { + $SetContent.Process("#Region 'PREFIX' -1`n") + $SetContent.Process($file) + $SetContent.Process("#EndRegion 'PREFIX'") + $ContentStarted = $true + } else { + $SetContent.Process("#Region 'SUFFIX' -1`n") + $SetContent.Process($file) + $SetContent.Process("#EndRegion 'SUFFIX'") + } + } + } + } + end { + $SetContent.End() + Pop-Location -StackName SetModuleContent + } +} +#EndRegion './Private/SetModuleContent.ps1' 73 +#Region './Public/Build-Module.ps1' -1 + +function Build-Module { + <# + .Synopsis + Compile a module from ps1 files to a single psm1 + + .Description + Compiles modules from source according to conventions: + 1. A single ModuleName.psd1 manifest file with metadata + 2. Source subfolders in the same directory as the Module manifest: + Enum, Classes, Private, Public contain ps1 files + 3. Optionally, a build.psd1 file containing settings for this function + + The optimization process: + 1. The OutputDirectory is created + 2. All psd1/psm1/ps1xml files (except build.psd1) in the Source will be copied to the output + 3. If specified, $CopyPaths (relative to the Source) will be copied to the output + 4. The ModuleName.psm1 will be generated (overwritten completely) by concatenating all .ps1 files in the $SourceDirectories subdirectories + 5. The ModuleVersion and ExportedFunctions in the ModuleName.psd1 may be updated (depending on parameters) + + .Example + Build-Module -Suffix "Export-ModuleMember -Function *-* -Variable PreferenceVariable" + + This example shows how to build a simple module from it's manifest, adding an Export-ModuleMember as a Suffix + + .Example + Build-Module -Prefix "using namespace System.Management.Automation" + + This example shows how to build a simple module from it's manifest, adding a using statement at the top as a prefix + + .Example + $gitVersion = gitversion | ConvertFrom-Json | Select -Expand InformationalVersion + Build-Module -SemVer $gitVersion + + This example shows how to use a semantic version from gitversion to version your build. + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "", Justification="Build is approved now")] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseCmdletCorrectly", "")] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "", Justification="Parameter handling is in InitializeBuild")] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidDefaultValueSwitchParameter", "", Justification = "VersionedOutputDirectory is Deprecated")] + [CmdletBinding(DefaultParameterSetName="SemanticVersion")] + [Alias("build")] + param( + # The path to the module folder, manifest or build.psd1 + [Parameter(Position = 0, ValueFromPipelineByPropertyName)] + [ValidateScript({ + if (Test-Path $_) { + $true + } else { + throw "Source must point to a valid module" + } + })] + [Alias("ModuleManifest", "Path")] + [string]$SourcePath = $(Get-Location -PSProvider FileSystem), + + # Where to build the module. Defaults to "../Output" adjacent to the "SourcePath" folder. + # The ACTUAL output may be in a subfolder of this path ending with the module name and version + # The default value is ../Output which results in the build going to ../Output/ModuleName/1.2.3 + [Alias("Destination")] + [string]$OutputDirectory = "../Output", + + # DEPRECATED. Now defaults true, producing a OutputDirectory with a version number as the last folder + [switch]$VersionedOutputDirectory = $true, + + # Overrides the VersionedOutputDirectory, producing an OutputDirectory without a version number as the last folder + [switch]$UnversionedOutputDirectory, + + # Semantic version, like 1.0.3-beta01+sha.22c35ffff166f34addc49a3b80e622b543199cc5 + # If the SemVer has metadata (after a +), then the full Semver will be added to the ReleaseNotes + [Parameter(ParameterSetName="SemanticVersion")] + [string]$SemVer, + + # The module version (must be a valid System.Version such as PowerShell supports for modules) + [Alias("ModuleVersion")] + [Parameter(ParameterSetName="ModuleVersion", Mandatory)] + [version]$Version = $(if(($V = $SemVer.Split("+")[0].Split("-",2)[0])){$V}), + + # Setting pre-release forces the release to be a pre-release. + # Must be valid pre-release tag like PowerShellGet supports + [Parameter(ParameterSetName="ModuleVersion")] + [string]$Prerelease = $($SemVer.Split("+")[0].Split("-",2)[1]), + + # Build metadata (like the commit sha or the date). + # If a value is provided here, then the full Semantic version will be inserted to the release notes: + # Like: ModuleName v(Version(-Prerelease?)+BuildMetadata) + [Parameter(ParameterSetName="ModuleVersion")] + [string]$BuildMetadata = $($SemVer.Split("+",2)[1]), + + # Folders which should be copied intact to the module output + # Can be relative to the module folder + [AllowEmptyCollection()] + [Alias("CopyDirectories")] + [string[]]$CopyPaths = @(), + + # Folders which contain source .ps1 scripts to be concatenated into the module + # Defaults to Enum, Classes, Private, Public + [string[]]$SourceDirectories = @( + "[Ee]num", "[Cc]lasses", "[Pp]rivate", "[Pp]ublic" + ), + + # A Filter (relative to the module folder) for public functions + # If non-empty, FunctionsToExport will be set with the file BaseNames of matching files + # Defaults to Public/*.ps1 + [AllowEmptyString()] + [string[]]$PublicFilter = "[Pp]ublic/*.ps1", + + # A switch that allows you to disable the update of the AliasesToExport + # By default, (if PublicFilter is not empty, and this is not set) + # Build-Module updates the module manifest FunctionsToExport and AliasesToExport + # with the combination of all the values in [Alias()] attributes on public functions + # and aliases created with `New-ALias` or `Set-Alias` at script level in the module + [Alias("IgnoreAliasAttribute")] + [switch]$IgnoreAlias, + + # File encoding for output RootModule (defaults to UTF8) + # Converted to System.Text.Encoding for PowerShell 6 (and something else for PowerShell 5) + [ValidateSet("UTF8", "UTF8Bom", "UTF8NoBom", "UTF7", "ASCII", "Unicode", "UTF32")] + [string]$Encoding = $(if($IsCoreCLR) { "UTF8Bom" } else { "UTF8" }), + + # The prefix is either the path to a file (relative to the module folder) or text to put at the top of the file. + # If the value of prefix resolves to a file, that file will be read in, otherwise, the value will be used. + # The default is nothing. See examples for more details. + [string]$Prefix, + + # The Suffix is either the path to a file (relative to the module folder) or text to put at the bottom of the file. + # If the value of Suffix resolves to a file, that file will be read in, otherwise, the value will be used. + # The default is nothing. See examples for more details. + [Alias("ExportModuleMember","Postfix")] + [string]$Suffix, + + # Controls whether we delete the output folder and whether we build the output + # There are three options: + # - Clean deletes the build output folder + # - Build builds the module output + # - CleanBuild first deletes the build output folder and then builds the module back into it + # Note that the folder to be deleted is the actual calculated output folder, with the version number + # So for the default OutputDirectory with version 1.2.3, the path to clean is: ../Output/ModuleName/1.2.3 + [ValidateSet("Clean", "Build", "CleanBuild")] + [string]$Target = "CleanBuild", + + # Output the ModuleInfo of the "built" module + [switch]$Passthru + ) + + begin { + if ($Encoding -notmatch "UTF8") { + Write-Warning "For maximum portability, we strongly recommend you build your script modules with UTF8 encoding (with a BOM, for backwards compatibility to PowerShell 5)." + } + } + process { + try { + # Push into the module source (it may be a subfolder) + $ModuleInfo = InitializeBuild $SourcePath + Write-Progress "Building $($ModuleInfo.Name)" -Status "Use -Verbose for more information" + Write-Verbose "Building $($ModuleInfo.Name)" + + # Ensure the OutputDirectory (exists for build, or is cleaned otherwise) + $OutputDirectory = $ModuleInfo | ResolveOutputFolder + if ($ModuleInfo.Target -notmatch "Build") { + return + } + $RootModule = Join-Path $OutputDirectory "$($ModuleInfo.Name).psm1" + $OutputManifest = Join-Path $OutputDirectory "$($ModuleInfo.Name).psd1" + Write-Verbose "Output to: $OutputDirectory" + + # Skip the build if it's up to date already + Write-Verbose "Target $($ModuleInfo.Target)" + $NewestBuild = (Get-Item $RootModule -ErrorAction SilentlyContinue).LastWriteTime + $IsNew = Get-ChildItem $ModuleInfo.ModuleBase -Recurse | + Where-Object LastWriteTime -gt $NewestBuild | + Select-Object -First 1 -ExpandProperty LastWriteTime + + if ($null -eq $IsNew) { + # This is mostly for testing ... + if ($ModuleInfo.Passthru) { + Get-Module $OutputManifest -ListAvailable + } + return # Skip the build + } + + # Note that the module manifest parent folder is the "root" of the source directories + Push-Location $ModuleInfo.ModuleBase -StackName Build-Module + + Write-Verbose "Copy files to $OutputDirectory" + # Copy the files and folders which won't be processed + Copy-Item *.psm1, *.psd1, *.ps1xml -Exclude "build.psd1" -Destination $OutputDirectory -Force + if ($ModuleInfo.CopyPaths) { + Write-Verbose "Copy Entire Directories: $($ModuleInfo.CopyPaths)" + Copy-Item -Path $ModuleInfo.CopyPaths -Recurse -Destination $OutputDirectory -Force + } + + Write-Verbose "Combine scripts to $RootModule" + + # SilentlyContinue because there don't *HAVE* to be functions at all + Write-Debug " SourceDirectories: $($ModuleInfo.ModuleBase) + $($ModuleInfo.SourceDirectories -join '|')" + $AllScripts = @($ModuleInfo.SourceDirectories).ForEach{ + # By explicitly converting, we support wildcards in the SourceDirectories parameter + if ($SourceDirectory = Join-Path -Path $ModuleInfo.ModuleBase -ChildPath $_ | Convert-Path -ErrorAction SilentlyContinue) { + Write-Debug " SourceDirectory: $SourceDirectory" + Get-ChildItem -Path $SourceDirectory -Filter *.ps1 -Recurse -ErrorAction SilentlyContinue | + Sort-Object -Property 'FullName' + } + } + + # We have to force the Encoding to string because PowerShell Core made up encodings + SetModuleContent -Source (@($ModuleInfo.Prefix) + $AllScripts.FullName + @($ModuleInfo.Suffix)).Where{$_} -Output $RootModule -Encoding "$($ModuleInfo.Encoding)" + + $ParseResult = ConvertToAst $RootModule + $ParseResult | MoveUsingStatements -Encoding "$($ModuleInfo.Encoding)" + + # If there is a PublicFilter, update ExportedFunctions + if ($ModuleInfo.PublicFilter) { + # SilentlyContinue because there don't *HAVE* to be public functions + if (($PublicFunctions = Get-ChildItem $ModuleInfo.PublicFilter -Recurse -ErrorAction SilentlyContinue | + Where-Object BaseName -in $AllScripts.BaseName | + Select-Object -ExpandProperty BaseName)) { + + Update-Metadata -Path $OutputManifest -PropertyName FunctionsToExport -Value $PublicFunctions + } + } + + # In order to support aliases to files, such as required by Invoke-Build, always export aliases + if (-not $ModuleInfo.IgnoreAlias) { + if (($AliasesToExport = $ParseResult | GetCommandAlias)) { + Update-Metadata -Path $OutputManifest -PropertyName AliasesToExport -Value $AliasesToExport + } + } + + try { + if ($ModuleInfo.Version) { + Write-Verbose "Update Manifest at $OutputManifest with version: $($ModuleInfo.Version)" + Update-Metadata -Path $OutputManifest -PropertyName ModuleVersion -Value $ModuleInfo.Version + } + } catch { + Write-Warning "Failed to update version to $($ModuleInfo.Version). $_" + } + + if ($null -ne (Get-Metadata -Path $OutputManifest -PropertyName PrivateData.PSData.Prerelease -ErrorAction SilentlyContinue)) { + if ($ModuleInfo.Prerelease) { + Write-Verbose "Update Manifest at $OutputManifest with Prerelease: $($ModuleInfo.Prerelease)" + Update-Metadata -Path $OutputManifest -PropertyName PrivateData.PSData.Prerelease -Value $ModuleInfo.Prerelease + } elseif ($PSCmdlet.ParameterSetName -eq "SemanticVersion" -or $PSBoundParameters.ContainsKey("Prerelease")) { + Update-Metadata -Path $OutputManifest -PropertyName PrivateData.PSData.Prerelease -Value "" + } + } elseif ($ModuleInfo.Prerelease) { + Write-Warning ("Cannot set Prerelease in module manifest. Add an empty Prerelease to your module manifest, like:`n" + + ' PrivateData = @{ PSData = @{ Prerelease = "" } }') + } + + if ($ModuleInfo.BuildMetadata) { + Write-Verbose "Update Manifest at $OutputManifest with metadata: $($ModuleInfo.BuildMetadata) from $($ModuleInfo.SemVer)" + $RelNote = Get-Metadata -Path $OutputManifest -PropertyName PrivateData.PSData.ReleaseNotes -ErrorAction SilentlyContinue + if ($null -ne $RelNote) { + $Line = "$($ModuleInfo.Name) v$($($ModuleInfo.SemVer))" + if ([string]::IsNullOrWhiteSpace($RelNote)) { + Write-Verbose "New ReleaseNotes:`n$Line" + Update-Metadata -Path $OutputManifest -PropertyName PrivateData.PSData.ReleaseNotes -Value $Line + } elseif ($RelNote -match "^\s*\n") { + # Leading whitespace includes newlines + Write-Verbose "Existing ReleaseNotes:$RelNote" + $RelNote = $RelNote -replace "^(?s)(\s*)\S.*$|^$","`${1}$($Line)`$_" + Write-Verbose "New ReleaseNotes:$RelNote" + Update-Metadata -Path $OutputManifest -PropertyName PrivateData.PSData.ReleaseNotes -Value $RelNote + } else { + Write-Verbose "Existing ReleaseNotes:`n$RelNote" + $RelNote = $RelNote -replace "^(?s)(\s*)\S.*$|^$","`${1}$($Line)`n`$_" + Write-Verbose "New ReleaseNotes:`n$RelNote" + Update-Metadata -Path $OutputManifest -PropertyName PrivateData.PSData.ReleaseNotes -Value $RelNote + } + } + } + + # This is mostly for testing ... + if ($ModuleInfo.Passthru) { + Get-Module $OutputManifest -ListAvailable + } + } finally { + Pop-Location -StackName Build-Module -ErrorAction SilentlyContinue + } + Write-Progress "Building $($ModuleInfo.Name)" -Completed + } +} +#EndRegion './Public/Build-Module.ps1' 282 +#Region './Public/Convert-Breakpoint.ps1' -1 + +function Convert-Breakpoint { + <# + .SYNOPSIS + Convert any breakpoints on source files to module files and vice-versa + #> + [CmdletBinding(DefaultParameterSetName="All")] + param( + [Parameter(ParameterSetName="Module")] + [switch]$ModuleOnly, + [Parameter(ParameterSetName="Source")] + [switch]$SourceOnly + ) + + if (!$SourceOnly) { + foreach ($ModuleBreakPoint in Get-PSBreakpoint | ConvertFrom-SourceLineNumber) { + Set-PSBreakpoint -Script $ModuleBreakPoint.Script -Line $ModuleBreakPoint.Line + if ($ModuleOnly) { + # TODO: | Remove-PSBreakpoint + } + } + } + + if (!$ModuleOnly) { + foreach ($SourceBreakPoint in Get-PSBreakpoint | ConvertTo-SourceLineNumber) { + if (!(Test-Path $SourceBreakPoint.SourceFile)) { + Write-Warning "Can't find source path: $($SourceBreakPoint.SourceFile)" + } else { + Set-PSBreakpoint -Script $SourceBreakPoint.SourceFile -Line $SourceBreakPoint.SourceLineNumber + } + if ($SourceOnly) { + # TODO: | Remove-PSBreakpoint + } + } + } +} +#EndRegion './Public/Convert-Breakpoint.ps1' 36 +#Region './Public/Convert-CodeCoverage.ps1' -1 + +function Convert-CodeCoverage { + <# + .SYNOPSIS + Convert the file name and line numbers from Pester code coverage of "optimized" modules to the source + .DESCRIPTION + Converts the code coverage line numbers from Pester to the source file paths. + The returned file name is always the relative path stored in the module. + .EXAMPLE + Invoke-Pester .\Tests -CodeCoverage (Get-ChildItem .\Output -Filter *.psm1).FullName -PassThru | + Convert-CodeCoverage -SourceRoot .\Source -Relative + + Runs pester tests from a "Tests" subfolder against an optimized module in the "Output" folder, + piping the results through Convert-CodeCoverage to render the code coverage misses with the source paths. + #> + param( + # The root of the source folder (for resolving source code paths) + [Parameter(Mandatory)] + [string]$SourceRoot, + + # The output of `Invoke-Pester -Pasthru` + # Note: Pester doesn't apply a custom type name + [Parameter(ValueFromPipeline)] + [PSObject]$InputObject + ) + process { + Push-Location $SourceRoot + try { + $InputObject.CodeCoverage.MissedCommands | ConvertTo-SourceLineNumber -Passthru | + Select-Object SourceFile, @{Name="Line"; Expr={$_.SourceLineNumber}}, Command + } finally { + Pop-Location + } + } +} +#EndRegion './Public/Convert-CodeCoverage.ps1' 35 +#Region './Public/ConvertFrom-SourceLineNumber.ps1' -1 + +function ConvertFrom-SourceLineNumber { + <# + .SYNOPSIS + Convert a source file path and line number to the line number in the built output + .EXAMPLE + ConvertFrom-SourceLineNumber -Module ~\2.0.0\ModuleBuilder.psm1 -SourceFile ~\Source\Public\Build-Module.ps1 -Line 27 + #> + [CmdletBinding(DefaultParameterSetName="FromString")] + param( + # The SourceFile is the source script file that was built into the module + [Parameter(Mandatory, ValueFromPipelineByPropertyName, Position=0)] + [Alias("PSCommandPath", "File", "ScriptName", "Script")] + [string]$SourceFile, + + # The SourceLineNumber (from an InvocationInfo) is the line number in the source file + [Parameter(Mandatory, ValueFromPipelineByPropertyName, Position=1)] + [Alias("LineNumber", "Line", "ScriptLineNumber")] + [int]$SourceLineNumber, + + # The name of the module in memory, or the full path to the module psm1 + [Parameter()] + [string]$Module + ) + begin { + $filemap = @{} + } + process { + if (!$Module) { + $Command = [IO.Path]::GetFileNameWithoutExtension($SourceFile) + $Module = (Get-Command $Command -ErrorAction SilentlyContinue).Source + if (!$Module) { + Write-Warning "Please specify -Module for ${SourceFile}: $SourceLineNumber" + return + } + } + if ($Module -and -not (Test-Path $Module)) { + $Module = (Get-Module $Module -ErrorAction Stop).Path + } + # Push-Location (Split-Path $SourceFile) + try { + if (!$filemap.ContainsKey($Module)) { + # Note: the new pattern is #Region but the old one was # BEGIN + $regions = Select-String '^(?:#Region|# BEGIN) (?.*) (?-?\d+)?$' -Path $Module + $filemap[$Module] = @($regions.ForEach{ + [PSCustomObject]@{ + PSTypeName = "BuildSourceMapping" + SourceFile = $_.Matches[0].Groups["SourceFile"].Value.Trim("'") + StartLineNumber = $_.LineNumber + # This offset is subtracted when calculating the line number + # because of the new line we're adding prior to the content + # of each script file in the built module. + Offset = $_.Matches[0].Groups["LineNumber"].Value + } + }) + } + + $hit = $filemap[$Module] + + if ($Source = $hit.Where{ $SourceFile.EndsWith($_.SourceFile.TrimStart(".\")) }) { + [PSCustomObject]@{ + PSTypeName = "OutputLocation" + Script = $Module + Line = $Source.StartLineNumber + $SourceLineNumber - $Source.Offset + } + } elseif($Source -eq $Module) { + [PSCustomObject]@{ + PSTypeName = "OutputLocation" + Script = $Module + Line = $SourceLineNumber - $Source.Offset + } + } else { + Write-Warning "'$SourceFile' not found in $Module" + } + } finally { + Pop-Location + } + } +} +#EndRegion './Public/ConvertFrom-SourceLineNumber.ps1' 79 +#Region './Public/ConvertTo-SourceLineNumber.ps1' -1 + +function ConvertTo-SourceLineNumber { + <# + .SYNOPSIS + Convert the line number in a built module to a file and line number in source + .EXAMPLE + ConvertTo-SourceLineNumber -SourceFile ~\ErrorMaker.psm1 -SourceLineNumber 27 + .EXAMPLE + ConvertTo-SourceLineNumber -PositionMessage "At C:\Users\Joel\OneDrive\Documents\PowerShell\Modules\ErrorMaker\ErrorMaker.psm1:27 char:4" + #> + [Alias("Convert-LineNumber")] + [CmdletBinding(DefaultParameterSetName="FromString")] + param( + # A position message as found in PowerShell's error messages, ScriptStackTrace, or InvocationInfo + [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName="FromString")] + [string]$PositionMessage, + + # The SourceFile (from an InvocationInfo) is the module psm1 path + [Parameter(Mandatory, ValueFromPipelineByPropertyName, Position=0, ParameterSetName="FromInvocationInfo")] + [Alias("PSCommandPath", "File", "ScriptName", "Script")] + [string]$SourceFile, + + # The SourceLineNumber (from an InvocationInfo) is the module line number + [Parameter(Mandatory, ValueFromPipelineByPropertyName, Position=1, ParameterSetName="FromInvocationInfo")] + [Alias("LineNumber", "Line", "ScriptLineNumber")] + [int]$SourceLineNumber, + + # The actual InvocationInfo + [Parameter(ValueFromPipeline, DontShow, ParameterSetName="FromInvocationInfo")] + [psobject]$InputObject, + + # If set, passes through the InputObject, overwriting the SourceFile and SourceLineNumber. + # Otherwise, creates a new SourceLocation object with just those properties. + [Parameter(ParameterSetName="FromInvocationInfo")] + [switch]$Passthru + ) + begin { + $filemap = @{} + } + process { + if ($PSCmdlet.ParameterSetName -eq "FromString") { + $Invocation = ParseLineNumber $PositionMessage + $SourceFile = $Invocation.SourceFile + $SourceLineNumber = $Invocation.SourceLineNumber + } + if (!(Test-Path $SourceFile)) { + throw "'$SourceFile' does not exist" + } + Push-Location (Split-Path $SourceFile) + try { + if (!$filemap.ContainsKey($SourceFile)) { + # Note: the new pattern is #Region but the old one was # BEGIN + $regions = Select-String '^(?:#Region|# BEGIN) (?.*) (?-?\d+)?$' -Path $SourceFile + if ($regions.Count -eq 0) { + Write-Warning "No SourceMap for $SourceFile" + return + } + $filemap[$SourceFile] = @($regions.ForEach{ + [PSCustomObject]@{ + PSTypeName = "BuildSourceMapping" + SourceFile = $_.Matches[0].Groups["SourceFile"].Value.Trim("'") + StartLineNumber = [System.Int32] $_.LineNumber + # This offset is added when calculating the line number + # because of the new line we're adding prior to the content + # of each script file in the built module. + Offset = $_.Matches[0].Groups["LineNumber"].Value + } + }) + } + + $hit = $filemap[$SourceFile] + + # These are all negative, because BinarySearch returns the match *after* the line we're searching for + # We need the match *before* the line we're searching for + # And we need it as a zero-based index: + $index = -2 - [Array]::BinarySearch($hit.StartLineNumber, $SourceLineNumber) + $Source = $hit[$index] + + if($Passthru) { + $InputObject | + Add-Member -MemberType NoteProperty -Name SourceFile -Value $Source.SourceFile -PassThru -Force | + Add-Member -MemberType NoteProperty -Name SourceLineNumber -Value ($SourceLineNumber - $Source.StartLineNumber + $Source.Offset) -PassThru -Force + } else { + [PSCustomObject]@{ + PSTypeName = "SourceLocation" + SourceFile = $Source.SourceFile + SourceLineNumber = $SourceLineNumber - $Source.StartLineNumber + $Source.Offset + } + } + } finally { + Pop-Location + } + } +} +#EndRegion './Public/ConvertTo-SourceLineNumber.ps1' 94 diff --git a/Tools/ModuleBuilder/3.1.8/PSGetModuleInfo.xml b/Tools/ModuleBuilder/3.1.8/PSGetModuleInfo.xml new file mode 100644 index 000000000000..f5cec11b3b14 --- /dev/null +++ b/Tools/ModuleBuilder/3.1.8/PSGetModuleInfo.xml @@ -0,0 +1,165 @@ + + + + Microsoft.PowerShell.Commands.PSRepositoryItemInfo + System.Management.Automation.PSCustomObject + System.Object + + + ModuleBuilder + 3.1.8 + Module + A module for authoring and building PowerShell modules + Joel Bennett + Jaykul + Copyright 2018 Joel Bennett +
2025-04-12T21:57:27+08:00
+ +
2025-12-08T22:53:42.0930546+08:00
+ + + + Microsoft.PowerShell.Commands.DisplayHintType + System.Enum + System.ValueType + System.Object + + DateTime + 2 + + +
+ + https://github.com/PoshCode/ModuleBuilder/blob/master/LICENSE + https://github.com/PoshCode/ModuleBuilder + https://github.com/PoshCode/ModuleBuilder/blob/resources/ModuleBuilder.png?raw=true + + + System.Object[] + System.Array + System.Object + + + Authoring + Build + Development + BestPractices + PSModule + PSEdition_Core + PSEdition_Desktop + + + + + System.Collections.Hashtable + System.Object + + + + Workflow + + + + + + + Cmdlet + + + + Command + + + + Build-Module + Convert-Breakpoint + Convert-CodeCoverage + ConvertFrom-SourceLineNumber + ConvertTo-SourceLineNumber + + + + + Function + + + + Build-Module + Convert-Breakpoint + Convert-CodeCoverage + ConvertFrom-SourceLineNumber + ConvertTo-SourceLineNumber + + + + + DscResource + + + + RoleCapability + + + + + + ModuleBuilder v3.1.8+Build.local.Branch.main.Sha.b4d5aaf9df98194aa7d40c46cd3d7ca787011c54.Date.20250412T215606_x000A_ Fix case sensitivity of defaults for SourceDirectories and PublicFilter + + + + + + System.Collections.Specialized.OrderedDictionary + System.Object + + + + Name + Configuration + + + CanonicalId + nuget:Configuration + + + + + + https://www.powershellgallery.com/api/v2 + PSGallery + NuGet + + + System.Management.Automation.PSCustomObject + System.Object + + + Copyright 2018 Joel Bennett + A module for authoring and building PowerShell modules + False + ModuleBuilder v3.1.8+Build.local.Branch.main.Sha.b4d5aaf9df98194aa7d40c46cd3d7ca787011c54.Date.20250412T215606_x000A_ Fix case sensitivity of defaults for SourceDirectories and PublicFilter + True + False + 26117 + 273822 + 20183 + 12/04/2025 9:57:27 PM +08:00 + 12/04/2025 9:57:27 PM +08:00 + 8/12/2025 2:50:00 PM +08:00 + Authoring Build Development BestPractices PSModule PSEdition_Core PSEdition_Desktop PSFunction_Build-Module PSCommand_Build-Module PSFunction_Convert-Breakpoint PSCommand_Convert-Breakpoint PSFunction_Convert-CodeCoverage PSCommand_Convert-CodeCoverage PSFunction_ConvertFrom-SourceLineNumber PSCommand_ConvertFrom-SourceLineNumber PSFunction_ConvertTo-SourceLineNumber PSCommand_ConvertTo-SourceLineNumber PSIncludes_Function + False + 2025-12-08T14:50:00Z + 3.1.8 + Joel Bennett + false + Module + ModuleBuilder.nuspec|ModuleBuilder.psd1|ModuleBuilder.psm1|en-US\about_ModuleBuilder.help.txt + 4775ad56-8f64-432f-8da7-87ddf7a34653 + 5.1 + PoshCode + + + C:\Users\Zac\Documents\PowerShell\Modules\ModuleBuilder\3.1.8 +
+
+
diff --git a/Tools/ModuleBuilder/3.1.8/en-US/about_ModuleBuilder.help.txt b/Tools/ModuleBuilder/3.1.8/en-US/about_ModuleBuilder.help.txt new file mode 100644 index 000000000000..7c7f588c86cd --- /dev/null +++ b/Tools/ModuleBuilder/3.1.8/en-US/about_ModuleBuilder.help.txt @@ -0,0 +1,88 @@ +TOPIC + about_ModuleBuilder + +SHORT DESCRIPTION + Common set of tools and patterns for module authoring by the community. + +LONG DESCRIPTION + This project is an attempt by a group of PowerShell MVPs and module authors to: + + - Build a common set of tools for module authoring + - Encourage a common pattern for organizing PowerShell module projects + - Promote best practices for authoring functions and modules + + To get started, make sure your module repository looks like the conventions described below, and + build it using the `Build-Module` command. It will create a versioned folder with an updated Module Manifest and + the PSM1 file. + + The module is opinionated and expects a few conventions to be respected so that it can: + - compile the module into a single PSM1 for improved performance + - execute Pester tests on the built artifact + - Report correct code coverage against the source's file and line numbers + (as it was before merging into the single PSM1) + - bootstrap your repository with required dependencies + + The conventions the module expects and recommends are: + 1. Create a "Source" folder in your repository with a "build.psd1" file and your module manifest in it + 2. In the "build.psd1", specify the relative Path to your module's manifest, e.g. `@{ Path = "ModuleBuilder.psd1"}` + 3. In your manifest, make sure the "FunctionsToExport" entry is not commented out. You can leave empty. + 5. Within your Source Folder, create the "Private" and "Public" folders for your functions + For each function of your module, create a file for it. The functions in "Public" will be exported from the module, + without the extention, so it's important to respect the Verb-Noun format for the name of the files. + + Here is an example from the ModuleBuilder repository. + + ModuleBuilder + ├───Source + │ │ build.psd1 + │ │ ModuleBuilder.psd1 + │ │ + │ ├───en-US + │ │ about_ModuleBuilder.help.txt + │ ├───Private + │ │ CopyHelp.ps1 + │ │ CopyReadme.ps1 + │ │ InitializeBuild.ps1 + │ │ ParameterValues.ps1 + │ │ ParseLineNumber.ps1 + │ │ ResolveModuleManifest.ps1 + │ │ ResolveModuleSource.ps1 + │ │ ResolveOutputFolder.ps1 + │ │ SetModuleContent.ps1 + │ └───Public + │ Build-Module.ps1 + │ Convert-CodeCoverage.ps1 + │ Convert-LineNumber.ps1 + └───Tests + ├───Private + │ [...] + └───Public + [...] + +EXAMPLES + PS C:\> Build-Module -SourcePath .\ModuleBuilder\Source\build.psd1 + + This will create a versioned folder of the module with ModuleBuilder.psm1 containing all functions + from the Private and Public folder, an updated ModuleBuilder.psd1 module manifest with the FunctionsToExport + correctly populated with all functions from the Public Folder. + + ModuleBuilder + └─── 1.0.0 + │ ModuleBuilder.psd1 + │ ModuleBuilder.psm1 + │ + └───en-US + about_ModuleBuilder.help.txt + + +NOTE: + Thank you to all those who contributed to this module, by writing code, sharing opinions, and provided feedback. + +TROUBLESHOOTING NOTE: + Look out on the Github repository for issues and new releases. + +SEE ALSO + - https://github.com/PoshCode/ModuleBuilder + +KEYWORDS + Module, Build, Task, Template From 431d29296def585602ede34296810881bc4d1d94 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Sun, 7 Jun 2026 13:30:34 +0800 Subject: [PATCH 160/202] extra module dep --- Tools/Build-DevApiModules.ps1 | 1 + Tools/Configuration/1.6.0/Configuration.psd1 | 77 ++ Tools/Configuration/1.6.0/Configuration.psm1 | 787 ++++++++++++++++++ Tools/Configuration/1.6.0/PSGetModuleInfo.xml | 162 ++++ 4 files changed, 1027 insertions(+) create mode 100644 Tools/Configuration/1.6.0/Configuration.psd1 create mode 100644 Tools/Configuration/1.6.0/Configuration.psm1 create mode 100644 Tools/Configuration/1.6.0/PSGetModuleInfo.xml diff --git a/Tools/Build-DevApiModules.ps1 b/Tools/Build-DevApiModules.ps1 index f8bcfcac166d..a24492e09c98 100644 --- a/Tools/Build-DevApiModules.ps1 +++ b/Tools/Build-DevApiModules.ps1 @@ -5,6 +5,7 @@ $repoRoot = Split-Path -Parent $toolsRoot $modulesRoot = Join-Path $repoRoot 'Modules' $outputRoot = Join-Path $repoRoot 'Output' +Import-Module -Name (Join-Path $toolsRoot 'Configuration\1.6.0\Configuration.psd1') -Force Import-Module -Name (Join-Path $toolsRoot 'ModuleBuilder\3.1.8\ModuleBuilder.psd1') -Force Write-Host "Repo root: $repoRoot" diff --git a/Tools/Configuration/1.6.0/Configuration.psd1 b/Tools/Configuration/1.6.0/Configuration.psd1 new file mode 100644 index 000000000000..597a38ca6f78 --- /dev/null +++ b/Tools/Configuration/1.6.0/Configuration.psd1 @@ -0,0 +1,77 @@ +@{ + +# Script module or binary module file associated with this manifest. +ModuleToProcess = 'Configuration.psm1' + +# Version number of this module. +ModuleVersion = '1.6.0' + +# ID used to uniquely identify this module +GUID = 'e56e5bec-4d97-4dfd-b138-abbaa14464a6' + +# Author of this module +Author = @('Joel Bennett') + +# Company or vendor of this module +CompanyName = 'HuddledMasses.org' + +# Copyright statement for this module +Copyright = 'Copyright (c) 2014-2021 by Joel Bennett, all rights reserved.' + +# Description of the functionality provided by this module +Description = 'A module for storing and reading configuration values, with full PS Data serialization, automatic configuration for modules and scripts, etc.' + +# Exports - populated by the build +FunctionsToExport = @('Export-Configuration','Get-ConfigurationPath','Get-ParameterValue','Import-Configuration','Import-ParameterConfiguration') +CmdletsToExport = @() +VariablesToExport = @() +AliasesToExport = 'Get-StoragePath' +RequiredModules = @('Metadata') + +# List of all files packaged with this module +FileList = @('.\Configuration.psd1','.\Configuration.psm1') + +PrivateData = @{ + # Allows overriding the default paths where Configuration stores it's configuration + # Within those folders, the module assumes a "powershell" folder and creates per-module configuration folders + PathOverride = @{ + # Where the user's personal configuration settings go. + # Highest presedence, overrides all other settings. + # Defaults to $Env:LocalAppData on Windows + # Defaults to $Env:XDG_CONFIG_HOME elsewhere ($HOME/.config/) + UserData = "" + # On some systems there are "roaming" user configuration stored in the user's profile. Overrides machine configuration + # Defaults to $Env:AppData on Windows + # Defaults to $Env:XDG_CONFIG_DIRS elsewhere (or $HOME/.local/share/) + EnterpriseData = "" + # Machine specific configuration. Overrides defaults, but is overriden by both user roaming and user local settings + # Defaults to $Env:ProgramData on Windows + # Defaults to /etc/xdg elsewhere + MachineData = "" + } + # PSData is module packaging and gallery metadata embedded in PrivateData + # It's for the PoshCode and PowerShellGet modules + # We had to do this because it's the only place we're allowed to extend the manifest + # https://connect.microsoft.com/PowerShell/feedback/details/421837 + PSData = @{ + # The semver pre-release version information + PreRelease = '' + + # Keyword tags to help users find this module via navigations and search. + Tags = @('Development','Configuration','Settings','Storage') + + # The web address of this module's project or support homepage. + ProjectUri = "https://github.com/PoshCode/Configuration" + + # The web address of this module's license. Points to a page that's embeddable and linkable. + LicenseUri = "http://opensource.org/licenses/MIT" + + # Release notes for this particular version of the module + ReleaseNotes = ' + - Extract the Metadata module + - Add support for arbitrary AllowedVariables + ' + } +} + +} diff --git a/Tools/Configuration/1.6.0/Configuration.psm1 b/Tools/Configuration/1.6.0/Configuration.psm1 new file mode 100644 index 000000000000..134613ca6bfa --- /dev/null +++ b/Tools/Configuration/1.6.0/Configuration.psm1 @@ -0,0 +1,787 @@ +#Region '.\Header\param.ps1' 0 +# Allows you to override the Scope storage paths (e.g. for testing) +param( + $Converters = @{}, + $EnterpriseData, + $UserData, + $MachineData +) + +if ($Converters.Count) { + Add-MetadataConverter $Converters +} +#EndRegion '.\Header\param.ps1' 12 +#Region '.\Private\InitializeStoragePaths.ps1' 0 +function InitializeStoragePaths { + [CmdletBinding()] + param( + $EnterpriseData, + $UserData, + $MachineData + ) + + $PathOverrides = $MyInvocation.MyCommand.Module.PrivateData.PathOverride + + # Where the user's personal configuration settings go. + # Highest presedence, overrides all other settings. + if ([string]::IsNullOrWhiteSpace($UserData)) { + if (!($UserData = $PathOverrides.UserData)) { + if ($IsLinux -or $IsMacOs) { + # Defaults to $Env:XDG_CONFIG_HOME on Linux or MacOS ($HOME/.config/) + if (!($UserData = $Env:XDG_CONFIG_HOME)) { + $UserData = Join-Path $HOME .config/ + } + } else { + # Defaults to $Env:LocalAppData on Windows + if (!($UserData = $Env:LocalAppData)) { + $UserData = [Environment]::GetFolderPath("LocalApplicationData") + } + } + } + } + + # On some systems there are "roaming" user configuration stored in the user's profile. Overrides machine configuration + if ([string]::IsNullOrWhiteSpace($EnterpriseData)) { + if (!($EnterpriseData = $PathOverrides.EnterpriseData)) { + if ($IsLinux -or $IsMacOs) { + # Defaults to the first value in $Env:XDG_CONFIG_DIRS on Linux or MacOS (or $HOME/.local/share/) + if (!($EnterpriseData = @($Env:XDG_CONFIG_DIRS -split ([IO.Path]::PathSeparator))[0] )) { + $EnterpriseData = Join-Path $HOME .local/share/ + } + } else { + # Defaults to $Env:AppData on Windows + if (!($EnterpriseData = $Env:AppData)) { + $EnterpriseData = [Environment]::GetFolderPath("ApplicationData") + } + } + } + } + + # Machine specific configuration. Overrides defaults, but is overriden by both user roaming and user local settings + if ([string]::IsNullOrWhiteSpace($MachineData)) { + if (!($MachineData = $PathOverrides.MachineData)) { + if ($IsLinux -or $IsMacOs) { + # Defaults to /etc/xdg elsewhere + $XdgConfigDirs = $Env:XDG_CONFIG_DIRS -split ([IO.Path]::PathSeparator) | Where-Object { $_ -and (Test-Path $_) } + if (!($MachineData = if ($XdgConfigDirs.Count -gt 1) { + $XdgConfigDirs[1] + })) { + $MachineData = "/etc/xdg/" + } + } else { + # Defaults to $Env:ProgramData on Windows + if (!($MachineData = $Env:ProgramAppData)) { + $MachineData = [Environment]::GetFolderPath("CommonApplicationData") + } + } + } + } + + Join-Path $EnterpriseData powershell + Join-Path $UserData powershell + Join-Path $MachineData powershell +} + +$EnterpriseData, $UserData, $MachineData = InitializeStoragePaths -EnterpriseData $EnterpriseData -UserData $UserData -MachineData $MachineData +#EndRegion '.\Private\InitializeStoragePaths.ps1' 72 +#Region '.\Private\ParameterBinder.ps1' 0 +function ParameterBinder { + if (!$Module) { + [System.Management.Automation.PSModuleInfo]$Module = . { + $Command = ($CallStack)[0].InvocationInfo.MyCommand + $mi = if ($Command.ScriptBlock -and $Command.ScriptBlock.Module) { + $Command.ScriptBlock.Module + } else { + $Command.Module + } + + if ($mi -and $mi.ExportedCommands.Count -eq 0) { + if ($mi2 = Get-Module $mi.ModuleBase -ListAvailable | Where-Object { ($_.Name -eq $mi.Name) -and $_.ExportedCommands } | Select-Object -First 1) { + $mi = $mi2 + } + } + $mi + } + } + + if (!$CompanyName) { + [String]$CompanyName = . { + if ($Module) { + $CName = $Module.CompanyName -replace "[$([Regex]::Escape(-join[IO.Path]::GetInvalidFileNameChars()))]", "_" + if ($CName -eq "Unknown" -or -not $CName) { + $CName = $Module.Author + if ($CName -eq "Unknown" -or -not $CName) { + $CName = "AnonymousModules" + } + } + $CName + } else { + "AnonymousScripts" + } + } + } + + if (!$Name) { + [String]$Name = $(if ($Module) { + $Module.Name + } <# else { ($CallStack)[0].InvocationInfo.MyCommand.Name } #>) + } + + if (!$DefaultPath -and $Module) { + [String]$DefaultPath = $(if ($Module) { + Join-Path $Module.ModuleBase Configuration.psd1 + }) + } +} +#EndRegion '.\Private\ParameterBinder.ps1' 49 +#Region '.\Public\Export-Configuration.ps1' 0 +function Export-Configuration { + <# + .Synopsis + Exports a configuration object to a specified path. + .Description + Exports the configuration object to a file, by default, in the Roaming AppData location + + NOTE: this exports the FULL configuration to this file, which will override both defaults and local machine configuration when Import-Configuration is used. + .Example + @{UserName = $Env:UserName; LastUpdate = [DateTimeOffset]::Now } | Export-Configuration + + This example shows how to use Export-Configuration in your module to cache some data. + + .Example + Get-Module Configuration | Export-Configuration @{UserName = $Env:UserName; LastUpdate = [DateTimeOffset]::Now } + + This example shows how to use Export-Configuration to export data for use in a specific module. + #> + # PSSCriptAnalyzer team refuses to listen to reason. See bugs: #194 #283 #521 #608 + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "")] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Callstack', Justification = 'This is referenced in ParameterBinder')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Module', Justification = 'This is referenced in ParameterBinder')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'DefaultPath', Justification = 'This is referenced in ParameterBinder')] + [CmdletBinding(DefaultParameterSetName = '__ModuleInfo', SupportsShouldProcess)] + param( + # Specifies the objects to export as metadata structures. + # Enter a variable that contains the objects or type a command or expression that gets the objects. + # You can also pipe objects to Export-Metadata. + [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)] + $InputObject, + + # Serialize objects as hashtables + [switch]$AsHashtable, + + # A callstack. You should not ever pass this. + # It is used to calculate the defaults for all the other parameters. + [Parameter(ParameterSetName = "__CallStack")] + [System.Management.Automation.CallStackFrame[]]$CallStack = $(Get-PSCallStack), + + # The Module you're importing configuration for + [Parameter(ParameterSetName = "__ModuleInfo", ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] + [System.Management.Automation.PSModuleInfo]$Module, + + + # An optional module qualifier (by default, this is blank) + [Parameter(ParameterSetName = "ManualOverride", Mandatory = $true, ValueFromPipelineByPropertyName = $true)] + [Alias("Author")] + [String]$CompanyName, + + # The name of the module or script + # Will be used in the returned storage path + [Parameter(ParameterSetName = "ManualOverride", Mandatory = $true, ValueFromPipelineByPropertyName = $true)] + [String]$Name, + + # DefaultPath is IGNORED. + # The parameter was here to match Import-Configuration, but it is meaningless in Export-Configuration + # The only reason I haven't removed it is that I don't want to break any code that might be using it. + # TODO: If we release a breaking changes Configuration 2.0, remove this parameter + [Parameter(ParameterSetName = "ManualOverride", ValueFromPipelineByPropertyName = $true)] + [Alias("ModuleBase")] + [String]$DefaultPath, + + # The scope to save at, defaults to Enterprise (which returns a path in "RoamingData") + [Parameter(ParameterSetName = "ManualOverride")] + [ValidateSet("User", "Machine", "Enterprise")] + [string]$Scope = "Enterprise", + + # The version for saved settings -- if set, will be used in the returned path + # NOTE: this is *NOT* calculated from the CallStack + [Version]$Version + ) + process { + . ParameterBinder + if (!$Name) { + throw "Could not determine the storage name, Export-Configuration should only be called from inside a script or module, or by piping ModuleInfo to it." + } + + $Parameters = @{ + CompanyName = $CompanyName + Name = $Name + } + if ($Version) { + $Parameters.Version = $Version + } + + $MachinePath = Get-ConfigurationPath @Parameters -Scope $Scope + + $ConfigurationPath = Join-Path $MachinePath "Configuration.psd1" + + $InputObject | Export-Metadata $ConfigurationPath -AsHashtable:$AsHashtable + } +} +#EndRegion '.\Public\Export-Configuration.ps1' 93 +#Region '.\Public\Get-ConfigurationPath.ps1' 0 +function Get-ConfigurationPath { + #.Synopsis + # Gets an storage path for configuration files and data + #.Description + # Gets an AppData (or roaming profile) or ProgramData path for configuration and data storage. The folder returned is guaranteed to exist (which means calling this function actually creates folders). + # + # Get-ConfigurationPath is designed to be called from inside a module function WITHOUT any parameters. + # + # If you need to call Get-ConfigurationPath from outside a module, you should pipe the ModuleInfo to it, like: + # Get-Module Powerline | Get-ConfigurationPath + # + # As a general rule, there are three scopes which result in three different root folders + # User: $Env:LocalAppData + # Machine: $Env:ProgramData + # Enterprise: $Env:AppData (which is the "roaming" folder of AppData) + # + #.NOTES + # 1. This command is primarily meant to be used in modules, to find a place where they can serialize data for storage. + # 2. It's techincally possible for more than one module to exist with the same name. + # The command uses the Author or Company as a distinguishing name. + # + #.Example + # $CacheFile = Join-Path (Get-ConfigurationPath) Data.clixml + # $Data | Export-CliXML -Path $CacheFile + # + # This example shows how to use Get-ConfigurationPath with Export-CliXML to cache data as clixml from inside a module. + [Alias("Get-StoragePath")] + # [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Callstack', Justification = 'This is referenced in ParameterBinder')] + # [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Module', Justification = 'This is referenced in ParameterBinder')] + [CmdletBinding(DefaultParameterSetName = '__ModuleInfo')] + param( + # The scope to save at, defaults to Enterprise (which returns a path in "RoamingData") + [ValidateSet("User", "Machine", "Enterprise")] + [string]$Scope = "Enterprise", + + # A callstack. You should not ever pass this. + # It is used to calculate the defaults for all the other parameters. + [Parameter(ParameterSetName = "__CallStack")] + [System.Management.Automation.CallStackFrame[]]$CallStack = $(Get-PSCallStack), + + # The Module you're importing configuration for + [Parameter(ParameterSetName = "__ModuleInfo", ValueFromPipeline = $true)] + [System.Management.Automation.PSModuleInfo]$Module, + + # An optional module qualifier (by default, this is blank) + [Parameter(ParameterSetName = "ManualOverride", Mandatory = $true, ValueFromPipelineByPropertyName = $true)] + [Alias("Author")] + [String]$CompanyName, + + # The name of the module or script + # Will be used in the returned storage path + [Parameter(ParameterSetName = "ManualOverride", Mandatory = $true, ValueFromPipelineByPropertyName = $true)] + [String]$Name, + + # The version for saved settings -- if set, will be used in the returned path + # NOTE: this is *NOT* calculated from the CallStack + [Version]$Version, + + # By default, Get-ConfigurationPath creates the folder if it doesn't already exist + # This switch allows overriding that behavior: if set, does not create missing paths + [Switch]$SkipCreatingFolder + ) + begin { + $PathRoot = $(switch ($Scope) { + "Enterprise" { + $EnterpriseData + } + "User" { + $UserData + } + "Machine" { + $MachineData + } + # This should be "Process" scope, but what does that mean? + # "AppDomain" { $MachineData } + default { + $EnterpriseData + } + }) + if (Test-Path $PathRoot) { + $PathRoot = Resolve-Path $PathRoot + } elseif (!$SkipCreatingFolder) { + Write-Warning "The $Scope path $PathRoot cannot be found" + } + } + + process { + . ParameterBinder + + if (!$Name) { + Write-Error "Empty Name ($Name) in $($PSCmdlet.ParameterSetName): $($PSBoundParameters | Format-List | Out-String)" + throw "Could not determine the storage name, Get-ConfigurationPath should only be called from inside a script or module." + } + $CompanyName = $CompanyName -replace "[$([Regex]::Escape(-join[IO.Path]::GetInvalidFileNameChars()))]", "_" + if ($CompanyName -and $CompanyName -ne "Unknown") { + $PathRoot = Join-Path $PathRoot $CompanyName + } + + $PathRoot = Join-Path $PathRoot $Name + + if ($Version) { + $PathRoot = Join-Path $PathRoot $Version + } + + if (Test-Path $PathRoot -PathType Leaf) { + throw "Cannot create folder for Configuration because there's a file in the way at $PathRoot" + } + + if (!$SkipCreatingFolder -and !(Test-Path $PathRoot -PathType Container)) { + $null = New-Item $PathRoot -Type Directory -Force + } + + # Note: this used to call Resolve-Path + $PathRoot + } +} +#EndRegion '.\Public\Get-ConfigurationPath.ps1' 117 +#Region '.\Public\Get-ParameterValue.ps1' 0 +function Get-ParameterValue { + <# + .SYNOPSIS + Get parameter values from PSBoundParameters + DefaultValues and optionally, a configuration file + .DESCRIPTION + This function gives command authors an easy way to combine default parameter values and actual arguments. + It also supports user-specified default parameter values loaded from a configuration file. + + It returns a hashtable (like PSBoundParameters) which combines these parameter defaults with parameter values passed by the caller. + #> + [CmdletBinding()] + param( + # The base name of a configuration file to read defaults from + # If specified, the command will read a ".psd1" file with this name + # Suggested Value: $MyInvocation.MyCommand.Noun + [string]$FromFile, + + # If your configuration file has defaults for multiple commands, pass + # the top-level key which contains defaults for this invocation + [string]$CommandKey, + + # Allows extending the valid variables which are allowed to be referenced in configuration + # BEWARE: This exposes the value of these variables in the calling context to the configuration file + # You are reponsible to only allow variables which you know are safe to share + [String[]]$AllowedVariables + ) + + $CallersInvocation = $PSCmdlet.SessionState.PSVariable.GetValue("MyInvocation") + $BoundParameters = @{} + $CallersInvocation.BoundParameters + $AllParameters = $CallersInvocation.MyCommand.Parameters + + if ($FromFile) { + $FromFile = [IO.Path]::ChangeExtension($FromFile, ".psd1") + } + + $FileDefaults = if ($FromFile -and (Test-Path $FromFile)) { + $MetadataOptions = @{ + AllowedVariables = $AllowedVariables + PSVariable = $PSCmdlet.SessionState.PSVariable + ErrorAction = "SilentlyContinue" + } + Write-Debug "Importing $FromFile" + $FileValues = Import-Metadata $FromFile @MetadataOptions + if ($CommandKey) { + $FileValues = $FileValues.$CommandKey + } + $FileValues + } else { + @{} + } + + # Don't support getting common parameters from the config file + $CommonParameters = [System.Management.Automation.Cmdlet]::CommonParameters + + [System.Management.Automation.Cmdlet]::OptionalCommonParameters + + # Layer the defaults below config below actual parameter values + foreach ($parameter in $AllParameters.GetEnumerator().Where({ $_.Key -notin $CommonParameters })) { + Write-Debug " Parameter: $($parameter.key)" + $key = $parameter.Key + + # Support parameter aliases in the config file by changing the alias to the parameter name + # If the value is not in the file defaults AND was not set by the user ... + if ($FromFile -and -not $FileDefaults.ContainsKey($key) -and -not $BoundParameters.ContainsKey($key)) { + # Check if any of the aliases are in the file defaults + Write-Debug " Aliases: $($parameter.Value.Aliases -join ', ')" + foreach ($k in @($parameter.Value.Aliases)) { + if ($null -ne $k -and $FileDefaults.ContainsKey($k)) { + Write-Debug " ... Update FileDefaults[$key] from $k" + $FileDefaults[$key] = $FileDefaults[$k] + $null = $FileDefaults.Remove($k) + break + } + } + } + + # Bound parameter values > build.psd1 values > default parameters values + if ($CallersInvocation) { + # If it's in the file defaults (now) AND it was not already set at a higher precedence + if ($FromFile -and $FileDefaults.ContainsKey($Parameter) -and -not ($BoundParameters.ContainsKey($Parameter))) { + Write-Debug "Export $Parameter = $($FileDefaults[$Parameter])" + $BoundParameters[$Parameter] = $FileDefaults[$Parameter] + # Set the variable in the _callers_ SessionState as well as our return hashtable + $PSCmdlet.SessionState.PSVariable.Set($Parameter, $FileDefaults[$Parameter]) + # If it's still NOT in the file defaults and was not already set, check if there's a default value + } elseif (-not $FileDefaults.ContainsKey($key) -and -not $BoundParameters.ContainsKey($key)) { + # Reading the current value of the $key variable returns either the bound parameter or the default + if ($null -ne ($value = $PSCmdlet.SessionState.PSVariable.Get($key).Value)) { + Write-Debug " From Default: $($BoundParameters[$key] -join ', ')" + if ($value -ne ($null -as $parameter.Value.ParameterType)) { + $BoundParameters[$key] = $value + } + } + # Otherwise, it was set by the user, or ... + } elseif ($BoundParameters[$key]) { + Write-Debug " From Parameter: $($BoundParameters[$key] -join ', ')" + # We'll set it from the file + } elseif ($FileDefaults[$key]) { + Write-Debug " From File: $($FileDefaults[$key] -join ', ')" + $BoundParameters[$key] = $FileDefaults[$key] + } + } + } + + $BoundParameters +} +#EndRegion '.\Public\Get-ParameterValue.ps1' 106 +#Region '.\Public\Import-Configuration.ps1' 0 +function Import-Configuration { + #.Synopsis + # Import the full, layered configuration for the module. + #.Description + # Imports the DefaultPath Configuration file, and then imports the Machine, Roaming (enterprise), and local config files, if they exist. + # Each configuration file is layered on top of the one before (so only needs to set values which are different) + #.Example + # $Configuration = Import-Configuration + # + # This example shows how to use Import-Configuration in your module to load the cached data + # + #.Example + # $Configuration = Get-Module Configuration | Import-Configuration + # + # This example shows how to use Import-Configuration in your module to load data cached for another module + # + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Callstack', Justification = 'This is referenced in ParameterBinder')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Module', Justification = 'This is referenced in ParameterBinder')] + # [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'DefaultPath', Justification = 'This is referenced in ParameterBinder')] + [CmdletBinding(DefaultParameterSetName = '__CallStack')] + param( + # A callstack. You should not ever pass this. + # It is used to calculate the defaults for all the other parameters. + [Parameter(ParameterSetName = "__CallStack")] + [System.Management.Automation.CallStackFrame[]]$CallStack = $(Get-PSCallStack), + + # The Module you're importing configuration for + [Parameter(ParameterSetName = "__ModuleInfo", ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] + [System.Management.Automation.PSModuleInfo]$Module, + + # An optional module qualifier (by default, this is blank) + [Parameter(ParameterSetName = "ManualOverride", Mandatory = $true, ValueFromPipelineByPropertyName = $true)] + [Alias("Author")] + [String]$CompanyName, + + # The name of the module or script + # Will be used in the returned storage path + [Parameter(ParameterSetName = "ManualOverride", Mandatory = $true, ValueFromPipelineByPropertyName = $true)] + [String]$Name, + + # The full path (including file name) of a default Configuration.psd1 file + # By default, this is expected to be in the same folder as your module manifest, or adjacent to your script file + [Parameter(ParameterSetName = "ManualOverride", ValueFromPipelineByPropertyName = $true)] + [Alias("ModuleBase")] + [String]$DefaultPath, + + # The version for saved settings -- if set, will be used in the returned path + # NOTE: this is *never* calculated, if you use version numbers, you must manage them on your own + [Version]$Version, + + # If set (and PowerShell version 4 or later) preserve the file order of configuration + # This results in the output being an OrderedDictionary instead of Hashtable + [Switch]$Ordered, + + # Allows extending the valid variables which are allowed to be referenced in configuration + # BEWARE: This exposes the value of these variables in the calling context to the configuration file + # You are reponsible to only allow variables which you know are safe to share + [String[]]$AllowedVariables + ) + begin { + # Write-Debug "Import-Configuration for module $Name" + } + process { + . ParameterBinder + + if (!$Name) { + throw "Could not determine the configuration name. When you are not calling Import-Configuration from a module, you must specify the -Author and -Name parameter" + } + + $MetadataOptions = @{ + AllowedVariables = $AllowedVariables + PSVariable = $PSCmdlet.SessionState.PSVariable + Ordered = $Ordered + ErrorAction = "Ignore" + } + + if ($DefaultPath -and (Test-Path $DefaultPath -Type Container)) { + $DefaultPath = Join-Path $DefaultPath Configuration.psd1 + } + + $Configuration = if ($DefaultPath -and (Test-Path $DefaultPath)) { + Import-Metadata $DefaultPath @MetadataOptions + } else { + @{} + } + # Write-Debug "Module Configuration: ($DefaultPath)`n$($Configuration | Out-String)" + + + $Parameters = @{ + CompanyName = $CompanyName + Name = $Name + } + if ($Version) { + $Parameters.Version = $Version + } + + $MachinePath = Get-ConfigurationPath @Parameters -Scope Machine -SkipCreatingFolder + $MachinePath = Join-Path $MachinePath Configuration.psd1 + $Machine = if (Test-Path $MachinePath) { + Import-Metadata $MachinePath @MetadataOptions + } else { + @{} + } + # Write-Debug "Machine Configuration: ($MachinePath)`n$($Machine | Out-String)" + + + $EnterprisePath = Get-ConfigurationPath @Parameters -Scope Enterprise -SkipCreatingFolder + $EnterprisePath = Join-Path $EnterprisePath Configuration.psd1 + $Enterprise = if (Test-Path $EnterprisePath) { + Import-Metadata $EnterprisePath @MetadataOptions + } else { + @{} + } + # Write-Debug "Enterprise Configuration: ($EnterprisePath)`n$($Enterprise | Out-String)" + + $LocalUserPath = Get-ConfigurationPath @Parameters -Scope User -SkipCreatingFolder + $LocalUserPath = Join-Path $LocalUserPath Configuration.psd1 + $LocalUser = if (Test-Path $LocalUserPath) { + Import-Metadata $LocalUserPath @MetadataOptions + } else { + @{} + } + # Write-Debug "LocalUser Configuration: ($LocalUserPath)`n$($LocalUser | Out-String)" + + $Configuration | Update-Object $Machine | + Update-Object $Enterprise | + Update-Object $LocalUser + } +} +#EndRegion '.\Public\Import-Configuration.ps1' 130 +#Region '.\Public\Import-ParameterConfiguration.ps1' 0 +function Import-ParameterConfiguration { + <# + .SYNOPSIS + Loads a metadata file based on the calling command name and combines the values there with the parameter values of the calling function. + .DESCRIPTION + This function gives command authors and users an easy way to let the default parameter values of the command be set by a configuration file in the folder you call it from. + + Normally, you have three places to get parameter values from. In priority order, they are: + - Parameters passed by the caller always win + - The PowerShell $PSDefaultParameterValues hashtable appears to the function as if the user passed it + - Default parameter values (defined in the function) + + If you call this command at the top of a function, it overrides (only) the default parameter values with + + - Values from a manifest file in the present working directory ($pwd) + .EXAMPLE + Given that you've written a script like: + + function New-User { + [CmdletBinding()] + param( + $FirstName, + $LastName, + $UserName, + $Domain, + $EMail, + $Department, + [hashtable]$Permissions + ) + Import-ParameterConfiguration -Recurse + # Possibly calculated based on (default) parameter values + if (-not $UserName) { $UserName = "$FirstName.$LastName" } + if (-not $EMail) { $EMail = "$UserName@$Domain" } + + # Lots of work to create the user's AD account, email, set permissions etc. + + # Output an object: + [PSCustomObject]@{ + PSTypeName = "MagicUser" + FirstName = $FirstName + LastName = $LastName + EMail = $EMail + Department = $Department + Permissions = $Permissions + } + } + + You could create a User.psd1 in a folder with just: + + @{ Domain = "HuddledMasses.org" } + + Now the following command would resolve the `User.psd1` + And the user would get an appropriate email address automatically: + + PS> New-User Joel Bennett + + FirstName : Joel + LastName : Bennett + EMail : Joel.Bennett@HuddledMasses.org + + .EXAMPLE + Import-ParameterConfiguration works recursively (up through parent folders) + + That means it reads config files in the same way git reads .gitignore, + with settings in the higher level files (up to the root?) being + overridden by those in lower level files down to the WorkingDirectory + + Following the previous example to a ridiculous conclusion, + we could automate creating users by creating a tree like: + + C:\HuddledMasses\Security\Admins\ with a User.psd1 in each folder: + + # C:\HuddledMasses\User.psd1: + @{ + Domain = "HuddledMasses.org" + } + + # C:\HuddledMasses\Security\User.psd1: + @{ + Department = "Security" + Permissions = @{ + Access = "User" + } + } + + # C:\HuddledMasses\Security\Admins\User.psd1 + @{ + Permissions = @{ + Access = "Administrator" + } + } + + And then switch to the Admins directory and run: + + PS> New-User Joel Bennett + + FirstName : Joel + LastName : Bennett + EMail : Joel.Bennett@HuddledMasses.org + Department : Security + Permissions : { Access = Administrator } + + .EXAMPLE + Following up on our earlier example, let's look at a way to use that -FileName parameter. + If you wanted to use a different configuration files than your Noun, you can pass the file name in. + + You could even use one of your parameters to generate the file name. If we modify the function like ... + + function New-User { + [CmdletBinding()] + param( + $FirstName, + $LastName, + $UserName, + $Domain, + $EMail, + $Department, + [hashtable]$Permissions + ) + Import-ParameterConfiguration -FileName "${Department}User.psd1" + # Possibly calculated based on (default) parameter values + if (-not $UserName) { $UserName = "$FirstName.$LastName" } + if (-not $EMail) { $EMail = "$UserName@$Domain" } + + # Lots of work to create the user's AD account and email etc. + [PSCustomObject]@{ + PSTypeName = "MagicUser" + FirstName = $FirstName + LastName = $LastName + EMail = $EMail + # Passthru for testing + Permissions = $Permissions + } + } + + Now you could create a `SecurityUser.psd1` + + @{ + Domain = "HuddledMasses.org" + Permissions = @{ + Access = "Administrator" + } + } + + And run: + + PS> New-User Joel Bennett -Department Security + #> + [CmdletBinding()] + param( + # The folder the configuration should be read from. Defaults to the current working directory + [string]$WorkingDirectory = $pwd, + # The name of the configuration file. + # The default value is your command's Noun, with the ".psd1" extention. + # So if you call this from a command named Build-Module, the noun is "Module" and the config $FileName is "Module.psd1" + [string]$FileName, + + # If set, considers configuration files in the parent, and it's parent recursively + [switch]$Recurse, + + # Allows extending the valid variables which are allowed to be referenced in configuration + # BEWARE: This exposes the value of these variables in the calling context to the configuration file + # You are reponsible to only allow variables which you know are safe to share + [String[]]$AllowedVariables + ) + + $CallersInvocation = $PSCmdlet.SessionState.PSVariable.GetValue("MyInvocation") + $BoundParameters = @{} + $CallersInvocation.BoundParameters + $AllParameters = $CallersInvocation.MyCommand.Parameters.Keys + if (-not $PSBoundParameters.ContainsKey("FileName")) { + $FileName = "$($CallersInvocation.MyCommand.Noun).psd1" + } + + $MetadataOptions = @{ + AllowedVariables = $AllowedVariables + PSVariable = $PSCmdlet.SessionState.PSVariable + ErrorAction = "SilentlyContinue" + } + + do { + $FilePath = Join-Path $WorkingDirectory $FileName + + Write-Debug "Initializing parameters for $($CallersInvocation.InvocationName) from $(Join-Path $WorkingDirectory $FileName)" + if (Test-Path $FilePath) { + $ConfiguredDefaults = Import-Metadata $FilePath @MetadataOptions + + foreach ($Parameter in $AllParameters) { + # If it's in the defaults AND it was not already set at a higher precedence + if ($ConfiguredDefaults.ContainsKey($Parameter) -and -not ($BoundParameters.ContainsKey($Parameter))) { + Write-Debug "Export $Parameter = $($ConfiguredDefaults[$Parameter])" + $BoundParameters.Add($Parameter, $ConfiguredDefaults[$Parameter]) + # This "SessionState" is the _callers_ SessionState, not ours + $PSCmdlet.SessionState.PSVariable.Set($Parameter, $ConfiguredDefaults[$Parameter]) + } + } + } + Write-Debug "Recurse:$Recurse -and $($BoundParameters.Count) of $($AllParameters.Count) Parameters and $WorkingDirectory" + } while ($Recurse -and ($AllParameters.Count -gt $BoundParameters.Count) -and ($WorkingDirectory = Split-Path $WorkingDirectory)) +} +#EndRegion '.\Public\Import-ParameterConfiguration.ps1' 200 diff --git a/Tools/Configuration/1.6.0/PSGetModuleInfo.xml b/Tools/Configuration/1.6.0/PSGetModuleInfo.xml new file mode 100644 index 000000000000..5aeaa96f694c --- /dev/null +++ b/Tools/Configuration/1.6.0/PSGetModuleInfo.xml @@ -0,0 +1,162 @@ + + + + Microsoft.PowerShell.Commands.PSRepositoryItemInfo + System.Management.Automation.PSCustomObject + System.Object + + + Configuration + 1.6.0 + Module + A module for storing and reading configuration values, with full PS Data serialization, automatic configuration for modules and scripts, etc. + Joel Bennett + Jaykul + Copyright (c) 2014-2021 by Joel Bennett, all rights reserved. +
2023-08-24T04:24:42+08:00
+ +
2025-12-08T22:53:41.8132697+08:00
+ + + + Microsoft.PowerShell.Commands.DisplayHintType + System.Enum + System.ValueType + System.Object + + DateTime + 2 + + +
+ + http://opensource.org/licenses/MIT + https://github.com/PoshCode/Configuration + + + + System.Object[] + System.Array + System.Object + + + Development + Configuration + Settings + Storage + PSModule + + + + + System.Collections.Hashtable + System.Object + + + + Workflow + + + + + + + Cmdlet + + + + Command + + + + Export-Configuration + Get-ConfigurationPath + Get-ParameterValue + Import-Configuration + Import-ParameterConfiguration + + + + + Function + + + + Export-Configuration + Get-ConfigurationPath + Get-ParameterValue + Import-Configuration + Import-ParameterConfiguration + + + + + DscResource + + + + RoleCapability + + + + + + - Extract the Metadata module_x000D__x000A_ - Add support for arbitrary AllowedVariables + + + + + + System.Collections.Specialized.OrderedDictionary + System.Object + + + + Name + Metadata + + + CanonicalId + nuget:Metadata + + + + + + https://www.powershellgallery.com/api/v2 + PSGallery + NuGet + + + System.Management.Automation.PSCustomObject + System.Object + + + Copyright (c) 2014-2021 by Joel Bennett, all rights reserved. + A module for storing and reading configuration values, with full PS Data serialization, automatic configuration for modules and scripts, etc. + False + - Extract the Metadata module_x000D__x000A_ - Add support for arbitrary AllowedVariables + True + True + 550723 + 925498 + 11586 + 24/08/2023 4:24:42 AM +08:00 + 24/08/2023 4:24:42 AM +08:00 + 8/12/2025 2:50:00 PM +08:00 + Development Configuration Settings Storage PSModule PSFunction_Export-Configuration PSCommand_Export-Configuration PSFunction_Get-ConfigurationPath PSCommand_Get-ConfigurationPath PSFunction_Get-ParameterValue PSCommand_Get-ParameterValue PSFunction_Import-Configuration PSCommand_Import-Configuration PSFunction_Import-ParameterConfiguration PSCommand_Import-ParameterConfiguration PSIncludes_Function + False + 2025-12-08T14:50:00Z + 1.6.0 + Joel Bennett + false + Module + Configuration.nuspec|Configuration.psd1|Configuration.psm1 + e56e5bec-4d97-4dfd-b138-abbaa14464a6 + HuddledMasses.org + + + C:\Users\Zac\Documents\PowerShell\Modules\Configuration\1.6.0 +
+
+
From 336909e8837a835b113a92d6db94b4b23d83e678 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Sun, 7 Jun 2026 13:32:43 +0800 Subject: [PATCH 161/202] extra module dep --- Tools/Build-DevApiModules.ps1 | 1 + Tools/Metadata/1.5.7/Metadata.psd1 | 50 + Tools/Metadata/1.5.7/Metadata.psm1 | 1210 ++++++++++++++++++++++ Tools/Metadata/1.5.7/PSGetModuleInfo.xml | 154 +++ 4 files changed, 1415 insertions(+) create mode 100644 Tools/Metadata/1.5.7/Metadata.psd1 create mode 100644 Tools/Metadata/1.5.7/Metadata.psm1 create mode 100644 Tools/Metadata/1.5.7/PSGetModuleInfo.xml diff --git a/Tools/Build-DevApiModules.ps1 b/Tools/Build-DevApiModules.ps1 index a24492e09c98..388cd4d7367c 100644 --- a/Tools/Build-DevApiModules.ps1 +++ b/Tools/Build-DevApiModules.ps1 @@ -5,6 +5,7 @@ $repoRoot = Split-Path -Parent $toolsRoot $modulesRoot = Join-Path $repoRoot 'Modules' $outputRoot = Join-Path $repoRoot 'Output' +Import-Module -Name (Join-Path $toolsRoot 'Metadata\1.5.7\Metadata.psd1') -Force Import-Module -Name (Join-Path $toolsRoot 'Configuration\1.6.0\Configuration.psd1') -Force Import-Module -Name (Join-Path $toolsRoot 'ModuleBuilder\3.1.8\ModuleBuilder.psd1') -Force diff --git a/Tools/Metadata/1.5.7/Metadata.psd1 b/Tools/Metadata/1.5.7/Metadata.psd1 new file mode 100644 index 000000000000..a8656a191695 --- /dev/null +++ b/Tools/Metadata/1.5.7/Metadata.psd1 @@ -0,0 +1,50 @@ +@{ + +# Script module or binary module file associated with this manifest. +ModuleToProcess = 'Metadata.psm1' + +# Version number of this module. +ModuleVersion = '1.5.7' + +# ID used to uniquely identify this module +GUID = 'c7505d40-646d-46b5-a440-8a81791c5d23' + +# Author of this module +Author = @('Joel Bennett') + +# Company or vendor of this module +CompanyName = 'HuddledMasses.org' + +# Copyright statement for this module +Copyright = 'Copyright (c) 2014-2021 by Joel Bennett, all rights reserved.' + +# Description of the functionality provided by this module +Description = 'A module for PowerShell data serialization' + +# This doesn't make it into the build output, so it's irrelevant +FunctionsToExport = @('Add-MetadataConverter','ConvertFrom-Metadata','ConvertTo-Metadata','Export-Metadata','Get-Metadata','Import-Metadata','Test-PSVersion','Update-Metadata','Update-Object') +CmdletsToExport = @() +VariablesToExport = @() +AliasesToExport = @('FromMetadata','ToMetadata','Get-ManifestValue','Update-Manifest') +PrivateData = @{ + PSData = @{ + # The semver pre-release version information + PreRelease = '' + + # Keyword tags to help users find this module via navigations and search. + Tags = @('Serialization', 'Metadata', 'Development', 'Configuration', 'Settings') + + # The web address of this module's project or support homepage. + ProjectUri = "https://github.com/PoshCode/Metadata" + + # The web address of this module's license. Points to a page that's embeddable and linkable. + LicenseUri = "http://opensource.org/licenses/MIT" + + # Release notes for this particular version of the module + ReleaseNotes = ' + Fixes PSObject serialization bug + ' + } +} + +} diff --git a/Tools/Metadata/1.5.7/Metadata.psm1 b/Tools/Metadata/1.5.7/Metadata.psm1 new file mode 100644 index 000000000000..262c1ae161d5 --- /dev/null +++ b/Tools/Metadata/1.5.7/Metadata.psm1 @@ -0,0 +1,1210 @@ +#Region '.\Header\00. param.ps1' 0 +param( + $Converters = @{} +) + +$ModuleManifestExtension = ".psd1" +#EndRegion '.\Header\00. param.ps1' 6 +#Region '.\Header\01. IPsMetadataSerializable.ps1' 0 +Add-Type -TypeDefinition @' +public interface IPsMetadataSerializable { + string ToPsMetadata(); + void FromPsMetadata(string Metadata); +} +'@ +#EndRegion '.\Header\01. IPsMetadataSerializable.ps1' 7 +#Region '.\Private\FindHashKeyValue.ps1' 0 +function FindHashKeyValue { + [CmdletBinding()] + param( + $SearchPath, + $Ast, + [string[]] + $CurrentPath = @() + ) + # Write-Debug "FindHashKeyValue: $SearchPath -eq $($CurrentPath -Join '.')" + if ($SearchPath -eq ($CurrentPath -Join '.') -or $SearchPath -eq $CurrentPath[-1]) { + return $Ast | + Add-Member NoteProperty HashKeyPath ($CurrentPath -join '.') -PassThru -Force | + Add-Member NoteProperty HashKeyName ($CurrentPath[-1]) -PassThru -Force + } + + if ($Ast.PipelineElements.Expression -is [System.Management.Automation.Language.HashtableAst] ) { + $KeyValue = $Ast.PipelineElements.Expression + foreach ($KV in $KeyValue.KeyValuePairs) { + $result = FindHashKeyValue $SearchPath -Ast $KV.Item2 -CurrentPath ($CurrentPath + $KV.Item1.Value) + if ($null -ne $result) { + $result + } + } + } +} +#EndRegion '.\Private\FindHashKeyValue.ps1' 26 +#Region '.\Private\ThrowError.ps1' 0 +# Utility to throw an errorrecord +function ThrowError { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidOverwritingBuiltInCmdlets", "")] + param + ( + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCmdlet] + $Cmdlet = $((Get-Variable -Scope 1 PSCmdlet).Value), + + [Parameter(Mandatory = $true, ParameterSetName = "ExistingException", Position = 1, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] + [Parameter(ParameterSetName = "NewException")] + [ValidateNotNullOrEmpty()] + [System.Exception] + $Exception, + + [Parameter(ParameterSetName = "NewException", Position = 2)] + [ValidateNotNullOrEmpty()] + [System.String] + $ExceptionType = "System.Management.Automation.RuntimeException", + + [Parameter(Mandatory = $true, ParameterSetName = "NewException", Position = 3)] + [ValidateNotNullOrEmpty()] + [System.String] + $Message, + + [Parameter(Mandatory = $false)] + [System.Object] + $TargetObject, + + [Parameter(Mandatory = $true, ParameterSetName = "ExistingException", Position = 10)] + [Parameter(Mandatory = $true, ParameterSetName = "NewException", Position = 10)] + [ValidateNotNullOrEmpty()] + [System.String] + $ErrorId, + + [Parameter(Mandatory = $true, ParameterSetName = "ExistingException", Position = 11)] + [Parameter(Mandatory = $true, ParameterSetName = "NewException", Position = 11)] + [ValidateNotNull()] + [System.Management.Automation.ErrorCategory] + $Category, + + [Parameter(Mandatory = $true, ParameterSetName = "Rethrow", Position = 1)] + [System.Management.Automation.ErrorRecord]$ErrorRecord + ) + process { + if (!$ErrorRecord) { + if ($PSCmdlet.ParameterSetName -eq "NewException") { + if ($Exception) { + $Exception = New-Object $ExceptionType $Message, $Exception + } else { + $Exception = New-Object $ExceptionType $Message + } + } + $errorRecord = New-Object System.Management.Automation.ErrorRecord $Exception, $ErrorId, $Category, $TargetObject + } + $Cmdlet.ThrowTerminatingError($errorRecord) + } +} +#EndRegion '.\Private\ThrowError.ps1' 60 +#Region '.\Private\WriteError.ps1' 0 +# Utility to throw an errorrecord +function WriteError { + param + ( + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCmdlet] + $Cmdlet = $((Get-Variable -Scope 1 PSCmdlet).Value), + + [Parameter(Mandatory = $true, ParameterSetName = "ExistingException", Position = 1, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] + [Parameter(ParameterSetName = "NewException")] + [ValidateNotNullOrEmpty()] + [System.Exception] + $Exception, + + [Parameter(ParameterSetName = "NewException", Position = 2)] + [ValidateNotNullOrEmpty()] + [System.String] + $ExceptionType = "System.Management.Automation.RuntimeException", + + [Parameter(Mandatory = $true, ParameterSetName = "NewException", Position = 3)] + [ValidateNotNullOrEmpty()] + [System.String] + $Message, + + [Parameter(Mandatory = $false)] + [System.Object] + $TargetObject, + + [Parameter(Mandatory = $true, Position = 10)] + [ValidateNotNullOrEmpty()] + [System.String] + $ErrorId, + + [Parameter(Mandatory = $true, Position = 11)] + [ValidateNotNull()] + [System.Management.Automation.ErrorCategory] + $Category, + + [Parameter(Mandatory = $true, ParameterSetName = "Rethrow", Position = 1)] + [System.Management.Automation.ErrorRecord]$ErrorRecord + ) + process { + if (!$ErrorRecord) { + if ($PSCmdlet.ParameterSetName -eq "NewException") { + if ($Exception) { + $Exception = New-Object $ExceptionType $Message, $Exception + } else { + $Exception = New-Object $ExceptionType $Message + } + } + $errorRecord = New-Object System.Management.Automation.ErrorRecord $Exception, $ErrorId, $Category, $TargetObject + } + $Cmdlet.WriteError($errorRecord) + } +} +#EndRegion '.\Private\WriteError.ps1' 57 +#Region '.\Public\Add-MetadataConverter.ps1' 0 +function Add-MetadataConverter { + <# + .Synopsis + Add a converter functions for serialization and deserialization to metadata + .Description + Add-MetadataConverter allows you to map: + * a type to a scriptblock which can serialize that type to metadata (psd1) + * define a name and scriptblock as a function which will be whitelisted in metadata (for ConvertFrom-Metadata and Import-Metadata) + + The idea is to give you a way to extend the serialization capabilities if you really need to. + .Example + Add-MetadataCOnverter @{ [bool] = { if($_) { '$True' } else { '$False' } } } + + Shows a simple example of mapping bool to a scriptblock that serializes it in a way that's inherently parseable by PowerShell. This exact converter is already built-in to the Metadata module, so you don't need to add it. + + .Example + Add-MetadataConverter @{ + [Uri] = { "Uri '$_' " } + "Uri" = { + param([string]$Value) + [Uri]$Value + } + } + + Shows how to map a function for serializing Uri objects as strings with a Uri function that just casts them. Normally you wouldn't need to do that for Uri, since they output strings natively, and it's perfectly logical to store Uris as strings and only cast them when you really need to. + + .Example + Add-MetadataConverter @{ + [DateTimeOffset] = { "DateTimeOffset {0} {1}" -f $_.Ticks, $_.Offset } + "DateTimeOffset" = {param($ticks,$offset) [DateTimeOffset]::new( $ticks, $offset )} + } + + Shows how to change the DateTimeOffset serialization. + + By default, DateTimeOffset values are (de)serialized using the 'o' RoundTrips formatting + e.g.: [DateTimeOffset]::Now.ToString('o') + + #> + [CmdletBinding()] + param( + # A hashtable of types to serializer scriptblocks, or function names to scriptblock definitions + [Parameter(Mandatory = $True)] + [hashtable]$Converters + ) + + if ($Converters.Count) { + switch ($Converters.Keys.GetEnumerator()) { + {$Converters[$_] -isnot [ScriptBlock]} { + WriteError -ExceptionType System.ArgumentExceptionn ` + -Message "Ignoring $_ converter, the value must be ScriptBlock!" ` + -ErrorId "NotAScriptBlock,Metadata\Add-MetadataConverter" ` + -Category "InvalidArgument" + continue + } + + {$_ -is [String]} { + # Write-Debug "Storing deserialization function: $_" + Set-Content "function:script:$_" $Converters[$_] + # We need to store the function in MetadataDeserializers + $script:MetadataDeserializers[$_] = $Converters[$_] + continue + } + + {$_ -is [Type]} { + # Write-Debug "Adding serializer for $($_.FullName)" + $script:MetadataSerializers[$_] = $Converters[$_] + continue + } + default { + WriteError -ExceptionType System.ArgumentExceptionn ` + -Message "Unsupported key type in Converters: $_ is $($_.GetType())" ` + -ErrorId "InvalidKeyType,Metadata\Add-MetadataConverter" ` + -Category "InvalidArgument" + } + } + } +} +#EndRegion '.\Public\Add-MetadataConverter.ps1' 78 +#Region '.\Public\ConvertFrom-Metadata.ps1' 0 +function ConvertFrom-Metadata { + <# + .Synopsis + Deserializes objects from PowerShell Data language (PSD1) + .Description + Converts psd1 notation to actual objects, and supports passing in additional converters + in addition to using the built-in registered converters (see Add-MetadataConverter). + + NOTE: Any Converters that are passed in are temporarily added as though passed Add-MetadataConverter + .Example + ConvertFrom-Metadata 'PSObject @{ Name = PSObject @{ First = "Joel"; Last = "Bennett" }; Id = 1; }' + + Id Name + -- ---- + 1 @{Last=Bennett; First=Joel} + + Convert the example string into a real PSObject using the built-in object serializer. + .Example + $data = ConvertFrom-Metadata .\Configuration.psd1 -Ordered + + Convert a module manifest into a hashtable of properties for introspection, preserving the order in the file + .Example + ConvertFrom-Metadata ("DateTimeOffset 635968680686066846 -05:00:00") -Converters @{ + "DateTimeOffset" = { + param($ticks,$offset) + [DateTimeOffset]::new( $ticks, $offset ) + } + } + + Shows how to temporarily add a "ValidCommand" called "DateTimeOffset" to support extra data types in the metadata. + + See also the third example on ConvertTo-Metadata and Add-MetadataConverter + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "", Justification = "Too late to call it Metadatum, LOL")] + [Alias('FromMetadata')] + [CmdletBinding(DefaultParameterSetName = "Legacy")] + param( + # The metadata text (or a path to a metadata file) + [Parameter(Mandatory = $true, Position = 0, ValueFromPipelineByPropertyName = "True")] + [Alias("PSPath")] + $InputObject, + + # The Type that the InputObject represents + [Parameter(Mandatory = $true, Position = 1, ParameterSetName = "FromPsMetadata")] + [Alias("As")] + [Type]$Type, + + # A hashtable of MetadataConverters (same as with Add-MetadataConverter) + [Hashtable]$Converters = @{}, + + # The PSScriptRoot which the metadata should be evaluated from. + # You do not normally need to pass this, and it has no effect unless + # you're referencing $ScriptRoot in your metadata + $ScriptRoot = "$PSScriptRoot", + + # If set (and PowerShell version 4 or later) preserve the file order of configuration + # This results in the output being an OrderedDictionary instead of Hashtable + [Switch]$Ordered, + + # Allows extending the valid variables which are allowed to be referenced in metadata + # BEWARE: This exposes the value of these variables in your context to the caller + # You are reponsible to only allow variables which you know are safe to share + [String[]]$AllowedVariables, + + # You should not pass this. + # The PSVariable parameter is for preserving variable scope within the Metadata commands + [System.Management.Automation.PSVariableIntrinsics]$PSVariable + ) + begin { + $OriginalMetadataSerializers = $Script:MetadataSerializers.Clone() + $OriginalMetadataDeserializers = $Script:MetadataDeserializers.Clone() + if ($Converters.Count) { + Add-MetadataConverter $Converters + } + [string[]]$ValidCommands = @( + "ConvertFrom-StringData", "ConvertFrom-Metadata", "Join-Path", "Split-Path", "ConvertTo-SecureString" + ) + @($MetadataDeserializers.Keys) + [string[]]$ValidVariables = $AllowedVariables + @( + "PSScriptRoot", "ScriptRoot", "PoshCodeModuleRoot", "PSCulture", "PSUICulture", "True", "False", "Null" + ) + + if (!$PSVariable) { + $PSVariable = $PSCmdlet.SessionState.PSVariable + } + } + end { + $Script:MetadataSerializers = $OriginalMetadataSerializers.Clone() + $Script:MetadataDeserializers = $OriginalMetadataDeserializers.Clone() + } + process { + $ErrorActionPreference = "Stop" + $Tokens = $Null; $ParseErrors = $Null + # Write-Debug "ParameterSet: $($PSCmdlet.ParameterSetName) $($PSBoundParameters | Format-Table | Out-String)" + if ($Type) { + try { + if ($Type.GetMethod("FromPsMetadata", [string])) { + $Output = New-Object $Type + $Output.FromPsMetadata($InputObject) + $Output + return + } else { + $Output = New-Object $Type -Property $InputObject + return + } + } catch { + # I almost want to suppress this and just try the fallback methods + Write-Warning "$($Type.FullName) failed using FromPsMetadata" + } + } + + if (Test-PSVersion -lt "3.0") { + # Write-Debug "ConvertFrom-Metadata: Using Import-LocalizedData to support PowerShell $($PSVersionTable.PSVersion)" + # Write-Debug "ConvertFrom-Metadata: $InputObject" + if (!(Test-Path $InputObject -ErrorAction SilentlyContinue)) { + $Path = [IO.path]::ChangeExtension([IO.Path]::GetTempFileName(), $ModuleManifestExtension) + Set-Content -Encoding UTF8 -Path $Path $InputObject + $InputObject = $Path + } elseif (!"$InputObject".EndsWith($ModuleManifestExtension)) { + $Path = [IO.path]::ChangeExtension([IO.Path]::GetTempFileName(), $ModuleManifestExtension) + Copy-Item "$InputObject" "$Path" + $InputObject = $Path + } + $Result = $null + Import-LocalizedData -BindingVariable Result -BaseDirectory (Split-Path $InputObject) -FileName (Split-Path $InputObject -Leaf) -SupportedCommand $ValidCommands + return $Result + } + + if (Test-Path $InputObject -ErrorAction SilentlyContinue) { + # Write-Debug "ConvertFrom-Metadata: Using ParseInput to support PowerShell $($PSVersionTable.PSVersion)" + # ParseFile on PS5 (and older) doesn't handle utf8 encoding properly (treats it as ASCII) + # Sometimes, that causes an avoidable error. So I'm avoiding it, if I can: + $Path = Convert-Path $InputObject + if (!$PSBoundParameters.ContainsKey('ScriptRoot')) { + $ScriptRoot = Split-Path $Path + } + $Content = (Get-Content -Path $InputObject -Encoding UTF8) + # Remove SIGnature blocks, PowerShell doesn't parse them in .psd1 and chokes on them here. + $Content = $Content -join "`n" -replace "# SIG # Begin signature block(?s:.*)" + try { + # But older versions of PowerShell, this will throw a MethodException because the overload is missing + $AST = [System.Management.Automation.Language.Parser]::ParseInput($Content, $Path, [ref]$Tokens, [ref]$ParseErrors) + } catch [System.Management.Automation.MethodException] { + # Write-Debug "ConvertFrom-Metadata: Using ParseFile as a backup for PowerShell $($PSVersionTable.PSVersion)" + $AST = [System.Management.Automation.Language.Parser]::ParseFile( $Path, [ref]$Tokens, [ref]$ParseErrors) + + # If we got parse errors on older versions of PowerShell, test to see if the error is just encoding + if ($null -ne $ParseErrors -and $ParseErrors.Count -gt 0) { + $StillErrors = $null + $AST = [System.Management.Automation.Language.Parser]::ParseInput($Content, [ref]$Tokens, [ref]$StillErrors) + # If we didn't get errors, clear the errors + # Otherwise, we want to use the original errors with the path in them + if ($null -eq $StillErrors -or $StillErrors.Count -eq 0) { + $ParseErrors = $StillErrors + } + } + } + } else { + # Write-Debug "ConvertFrom-Metadata: Using ParseInput with loose metadata: $InputObject" + if (!$PSBoundParameters.ContainsKey('ScriptRoot')) { + $ScriptRoot = $PoshCodeModuleRoot + } + + $OFS = "`n" + # Remove SIGnature blocks, PowerShell doesn't parse them in .psd1 and chokes on them here. + $InputObject = "$InputObject" -replace "# SIG # Begin signature block(?s:.*)" + $AST = [System.Management.Automation.Language.Parser]::ParseInput($InputObject, [ref]$Tokens, [ref]$ParseErrors) + } + + if ($null -ne $ParseErrors -and $ParseErrors.Count -gt 0) { + ThrowError -Exception (New-Object System.Management.Automation.ParseException (, [System.Management.Automation.Language.ParseError[]]$ParseErrors)) -ErrorId "Metadata Error" -Category "ParserError" -TargetObject $InputObject + } + + # Get the variables or subexpressions from strings which have them ("StringExpandable" vs "String") ... + $Tokens += $Tokens | Where-Object { "StringExpandable" -eq $_.Kind } | Select-Object -ExpandProperty NestedTokens + + # Write-Debug "ConvertFrom-Metadata: Searching $($Tokens.Count) variables for: $($ValidVariables -join ', '))" + # Work around PowerShell rules about magic variables + # Change all the "ValidVariables" to use names like __Metadata__OriginalName__ + # Later, we'll try to make sure these are all set! + if (($UsedVariables = $Tokens | Where-Object { ("Variable" -eq $_.Kind) -and ($_.Name -in $ValidVariables) -and ($_.Name -notin "PSCulture", "PSUICulture", "True", "False", "Null") })) { + # Write-Debug "ConvertFrom-Metadata: Replacing $($UsedVariables.Name -join ', ')" + if (($extents = @( $UsedVariables | ForEach-Object { $_.Extent | Add-Member NoteProperty Name $_.Name -PassThru } ))) { + $ScriptContent = $Ast.ToString() + # Write-Debug "ConvertFrom-Metadata: Replacing $($UsedVariables.Count) variables in metadata: $ScriptContent" + for ($r = $extents.count - 1; $r -ge 0; $r--) { + $VariableExtent = $extents[$r] + $VariableName = if ($VariableExtent.Name -eq "PSScriptRoot") { + '${__Metadata__ScriptRoot__}' + } else { + '${__Metadata__' + $VariableExtent.Name + '__}' + } + $ScriptContent = $ScriptContent.Remove( $VariableExtent.StartOffset, + ($VariableExtent.EndOffset - $VariableExtent.StartOffset) + ).Insert($VariableExtent.StartOffset, $VariableName) + } + } + # Write-Debug "ConvertFrom-Metadata: Replaced $($UsedVariables.Name -join ' and ') in metadata: $ScriptContent" + $AST = [System.Management.Automation.Language.Parser]::ParseInput($ScriptContent, [ref]$Tokens, [ref]$ParseErrors) + } + + $Script = $AST.GetScriptBlock() + try { + [string[]]$PrivateVariables = $ValidVariables -replace "^.*$", '__Metadata__$0__' + # Write-Debug "ConvertFrom-Metadata: Validating metadata: $Script against $PrivateVariables" + $Script.CheckRestrictedLanguage( $ValidCommands, $PrivateVariables, $true ) + } catch { + ThrowError -Exception $_.Exception.InnerException -ErrorId "Metadata Error" -Category "InvalidData" -TargetObject $Script + } + + # Set the __Metadata__ValidVariables__ in our scope but not for constant variables: + $ReplacementVariables = $ValidVariables | Where-Object { $_ -notin "PSCulture", "PSUICulture", "True", "False", "Null" } + foreach ($name in $ReplacementVariables) { + # We already read the script root from the calling scope ... + if ($Name -in "PSScriptRoot", "ScriptRoot", "PoshCodeModuleRoot") { + $Value = $ScriptRoot + } elseif (!($Value = $PSVariable.GetValue($Name))) { + $Value = "`${$Name}" + } + # Write-Debug "ConvertFrom-Metadata: Setting __Metadata__${Name}__ = $Value" + Set-Variable "__Metadata__${Name}__" $Value + } + + if ($Ordered -and (Test-PSVersion -gt "3.0")) { + # Write-Debug "ConvertFrom-Metadata: Supporting [Ordered] on PowerShell $($PSVersionTable.PSVersion)" + # Make all the hashtables ordered, so that the output objects make more sense to humans... + if ($Tokens | Where-Object { "AtCurly" -eq $_.Kind }) { + $ScriptContent = $AST.ToString() + $Hashtables = $AST.FindAll( {$args[0] -is [System.Management.Automation.Language.HashtableAst] -and ("ordered" -ne $args[0].Parent.Type.TypeName)}, $Recurse) + $Hashtables = $Hashtables | ForEach-Object { + New-Object PSObject -Property @{Type = "([ordered]"; Position = $_.Extent.StartOffset} + New-Object PSObject -Property @{Type = ")"; Position = $_.Extent.EndOffset} + } | Sort-Object Position -Descending + foreach ($point in $Hashtables) { + $ScriptContent = $ScriptContent.Insert($point.Position, $point.Type) + } + + $AST = [System.Management.Automation.Language.Parser]::ParseInput($ScriptContent, [ref]$Tokens, [ref]$ParseErrors) + $Script = $AST.GetScriptBlock() + } + } + # Write-Debug "ConvertFrom-Metadata: Metadata: $Script" + # Write-Debug "ConvertFrom-Metadata: Switching to RestrictedLanguage mode" + $Mode, $ExecutionContext.SessionState.LanguageMode = $ExecutionContext.SessionState.LanguageMode, "RestrictedLanguage" + + try { + $Script.InvokeReturnAsIs(@()) + } finally { + $ExecutionContext.SessionState.LanguageMode = $Mode + # Write-Debug "ConvertFrom-Metadata: Switching to $Mode mode" + } + } +} +#EndRegion '.\Public\ConvertFrom-Metadata.ps1' 253 +#Region '.\Public\ConvertTo-Metadata.ps1' 0 +function ConvertTo-Metadata { + #.Synopsis + # Serializes objects to PowerShell Data language (PSD1) + #.Description + # Converts objects to a texual representation that is valid in PSD1, + # using the built-in registered converters (see Add-MetadataConverter). + # + # NOTE: Any Converters that are passed in are temporarily added as though passed Add-MetadataConverter + #.Example + # $Name = @{ First = "Joel"; Last = "Bennett" } + # @{ Name = $Name; Id = 1; } | ConvertTo-Metadata + # + # @{ + # Id = 1 + # Name = @{ + # Last = 'Bennett' + # First = 'Joel' + # } + # } + # + # Convert input objects into a formatted string suitable for storing in a psd1 file. + #.Example + # Get-ChildItem -File | Select-Object FullName, *Utc, Length | ConvertTo-Metadata + # + # Convert complex custom types to dynamic PSObjects using Select-Object. + # + # ConvertTo-Metadata understands PSObjects automatically, so this allows us to proceed + # without a custom serializer for File objects, but the serialized data + # will not be a FileInfo or a DirectoryInfo, just a custom PSObject + #.Example + # ConvertTo-Metadata ([DateTimeOffset]::Now) -Converters @{ + # [DateTimeOffset] = { "DateTimeOffset {0} {1}" -f $_.Ticks, $_.Offset } + # } + # + # Shows how to temporarily add a MetadataConverter to convert a specific type while serializing the current DateTimeOffset. + # Note that this serialization would require a "DateTimeOffset" function to exist in order to deserialize properly. + # + # See also the third example on ConvertFrom-Metadata and Add-MetadataConverter. + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "", Justification = "Too late to call it Metadatum, LOL")] + [Alias('ToMetadata')] + [OutputType([string])] + [CmdletBinding()] + param( + # The object to convert to metadata + [Parameter(ValueFromPipeline = $True)] + $InputObject, + + # Serialize objects as hashtables + [switch]$AsHashtable, + + # Additional converters + [Hashtable]$Converters = @{} + ) + begin { + if ($t -is [string] -and [string]::IsNullOrWhiteSpace($t)) { + # DO NOT USE += BECAUSE POWERSHELL WILL OPTIMIZE THIS AWAY + $t = $t + " " + } else { + $t = " " + } + $Script:OriginalMetadataSerializers = $Script:MetadataSerializers.Clone() + $Script:OriginalMetadataDeserializers = $Script:MetadataDeserializers.Clone() + Add-MetadataConverter $Converters + } + end { + $Script:MetadataSerializers = $Script:OriginalMetadataSerializers.Clone() + $Script:MetadataDeserializers = $Script:OriginalMetadataDeserializers.Clone() + } + process { + if ($Null -eq $InputObject) { + '""' + return + } + + if ($InputObject -is [IPsMetadataSerializable] -or + ($InputObject.ToPsMetadata -is [System.Management.Automation.PSMethod] -and + $InputObject.FromPsMetadata -is [System.Management.Automation.PSMethod])) { + try { + $result = "(ConvertFrom-Metadata @'`n{1}`n'@ -As {0})" -f $InputObject.GetType().FullName, $InputObject.ToPsMetadata() + if ($result -is [string]) { + $result + return + } + } catch { + <# The way we handle this is to #> + Write-Warning "InputObject of type $($InputObject.GetType().FullName) looks IMetadataSerializable, but threw an exception." + } + } + + if ($InputObject -is [Int16] -or + $InputObject -is [Int32] -or + $InputObject -is [Int64] -or + $InputObject -is [Double] -or + $InputObject -is [Decimal] -or + $InputObject -is [Byte] ) { + "$InputObject" + } elseif ($InputObject -is [String]) { + "'{0}'" -f $InputObject.ToString().Replace("'", "''") + } elseif ($InputObject -is [Collections.IDictionary]) { + "@{{`n{0}`n$($t -replace " $")}}" -f ($( + ForEach ($key in @($InputObject.Keys)) { + if ("$key" -match '^([A-Za-z_]\w*|-?\d+\.?\d*)$') { + "$t$key = " + (ConvertTo-Metadata $InputObject[$key] -AsHashtable:$AsHashtable) + } else { + "$t'$key' = " + (ConvertTo-Metadata $InputObject[$key] -AsHashtable:$AsHashtable) + } + }) -split "`n" -join "`n") + } elseif ($InputObject -is [System.Collections.IEnumerable]) { + "@($($(ForEach($item in @($InputObject)) { $item | ConvertTo-Metadata -AsHashtable:$AsHashtable}) -join ","))" + } elseif($InputObject -is [System.Management.Automation.ScriptBlock]) { + # Escape single-quotes by doubling them: + "(ScriptBlock '{0}')" -f ("$InputObject" -replace "'", "''") + } elseif ($InputObject.GetType().FullName -eq 'System.Management.Automation.PSCustomObject') { + # NOTE: we can't put [ordered] here because we need support for PS v2, but it's ok, because we put it in at parse-time + $(if ($AsHashtable) { + "@{{`n$t{0}`n}}" + } else { + "(PSObject @{{`n$t{0}`n}} -TypeName '$($InputObject.PSTypeNames -join "','")')" + }) -f ($( + ForEach ($key in $InputObject | Get-Member -MemberType Properties | Select-Object -ExpandProperty Name) { + if ("$key" -match '^([A-Za-z_]\w*|-?\d+\.?\d*)$') { + "$key = " + (ConvertTo-Metadata $InputObject.($key) -AsHashtable:$AsHashtable) + } else { + "'$($key -replace "'","''")' = " + (ConvertTo-Metadata $InputObject.($key) -AsHashtable:$AsHashtable) + } + } + ) -split "`n" -join "`n") + } elseif ($MetadataSerializers.ContainsKey($InputObject.GetType())) { + $Str = ForEach-Object $MetadataSerializers.($InputObject.GetType()) -InputObject $InputObject + + [bool]$IsCommand = & { + $ErrorActionPreference = "Stop" + $Tokens = $Null; $ParseErrors = $Null + $AST = [System.Management.Automation.Language.Parser]::ParseInput( $Str, [ref]$Tokens, [ref]$ParseErrors) + $Null -ne $Ast.Find( {$args[0] -is [System.Management.Automation.Language.CommandAst]}, $false) + } + + if ($IsCommand) { "($Str)" } else { $Str } + } else { + Write-Warning "$($InputObject.GetType().FullName) is not serializable. Serializing as string" + "'{0}'" -f $InputObject.ToString().Replace("'", "`'`'") + } + } +} +#EndRegion '.\Public\ConvertTo-Metadata.ps1' 145 +#Region '.\Public\Export-Metadata.ps1' 0 +function Export-Metadata { + <# + .Synopsis + Creates a metadata file from a simple object + .Description + Serves as a wrapper for ConvertTo-Metadata to explicitly support exporting to files + + Note that exportable data is limited by the rules of data sections (see about_Data_Sections) and the available MetadataSerializers (see Add-MetadataConverter) + + The only things inherently importable in PowerShell metadata files are Strings, Booleans, and Numbers ... and Arrays or Hashtables where the values (and keys) are all strings, booleans, or numbers. + + Note: this function and the matching Import-Metadata are extensible, and have included support for PSCustomObject, Guid, Version, etc. + .Example + $Configuration | Export-Metadata .\Configuration.psd1 + + Export a configuration object (or hashtable) to the default Configuration.psd1 file for a module + the metadata module uses Configuration.psd1 as it's default config file. + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "", Justification = "Too late to call it Metadatum, LOL")] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "")] # Because PSSCriptAnalyzer team refuses to listen to reason. See bugs: #194 #283 #521 #608 + [CmdletBinding(SupportsShouldProcess)] + param( + # Specifies the path to the PSD1 output file. + [Parameter(Mandatory = $true, Position = 0)] + $Path, + + # comments to place on the top of the file (to explain settings or whatever for people who might edit it by hand) + [string[]]$CommentHeader, + + # Specifies the objects to export as metadata structures. + # Enter a variable that contains the objects or type a command or expression that gets the objects. + # You can also pipe objects to Export-Metadata. + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + $InputObject, + + # Serialize objects as hashtables + [switch]$AsHashtable, + + [Hashtable]$Converters = @{}, + + # If set, output the nuspec file + [Switch]$Passthru + ) + begin { + $data = @() + } + process { + $data += @($InputObject) + } + end { + # Avoid arrays when they're not needed: + if ($data.Count -eq 1) { + $data = $data[0] + } + Set-Content -Encoding UTF8 -Path $Path -Value ((@($CommentHeader) + @(ConvertTo-Metadata -InputObject $data -Converters $Converters -AsHashtable:$AsHashtable)) -Join "`n") + if ($Passthru) { + Get-Item $Path + } + } +} +#EndRegion '.\Public\Export-Metadata.ps1' 61 +#Region '.\Public\Get-Metadata.ps1' 0 +function Get-Metadata { + #.Synopsis + # Reads a specific value from a PowerShell metadata file (e.g. a module manifest) + #.Description + # By default Get-Metadata gets the ModuleVersion, but it can read any key in the metadata file + #.Example + # Get-Metadata .\Configuration.psd1 + # + # Returns the module version number (as a string) + #.Example + # Get-Metadata .\Configuration.psd1 ReleaseNotes + # + # Returns the release notes! + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "", Justification = "Too late to call it Metadatum, LOL")] + [Alias("Get-ManifestValue")] + [CmdletBinding()] + param( + # The path to the module manifest file + [Parameter(ValueFromPipelineByPropertyName = "True", Position = 0, Mandatory)] + [Alias("PSPath")] + [ValidateScript({ + if ([IO.Path]::GetExtension($_) -ne ".psd1") { + throw "Path must point to a .psd1 file" + } + $true + })] + [string]$Path, + + # The property (or dotted property path) to be read from the manifest. + # Get-Metadata searches the Manifest root properties, and also the nested hashtable properties. + [Parameter(ParameterSetName = "Overwrite", Position = 1)] + [string]$PropertyName = 'ModuleVersion', + + [switch]$Passthru + ) + process { + $ErrorActionPreference = "Stop" + + if (!(Test-Path $Path)) { + WriteError -ExceptionType System.Management.Automation.ItemNotFoundException ` + -Message "Can't find file $Path" ` + -ErrorId "PathNotFound,Metadata\Get-Metadata" ` + -Category "ObjectNotFound" + return + } + $Path = Convert-Path $Path + + $Tokens = $Null; $ParseErrors = $Null + $AST = [System.Management.Automation.Language.Parser]::ParseFile( $Path, [ref]$Tokens, [ref]$ParseErrors ) + + $KeyValue = $Ast.EndBlock.Statements + $KeyValue = @(FindHashKeyValue $PropertyName $KeyValue) + if ($KeyValue.Count -eq 0) { + WriteError -ExceptionType System.Management.Automation.ItemNotFoundException ` + -Message "Can't find '$PropertyName' in $Path" ` + -ErrorId "PropertyNotFound,Metadata\Get-Metadata" ` + -Category "ObjectNotFound" + return + } + if ($KeyValue.Count -gt 1) { + $SingleKey = @($KeyValue | Where-Object { $_.HashKeyPath -eq $PropertyName }) + + if ($SingleKey.Count -ne 1) { + WriteError -ExceptionType System.Reflection.AmbiguousMatchException ` + -Message ("Found more than one '$PropertyName' in $Path. Please specify a dotted path instead. Matching paths include: '{0}'" -f ($KeyValue.HashKeyPath -join "', '")) ` + -ErrorId "AmbiguousMatch,Metadata\Get-Metadata" ` + -Category "InvalidArgument" + return + } else { + $KeyValue = $SingleKey + } + } + $KeyValue = $KeyValue[0] + + if ($Passthru) { + $KeyValue + } else { + # # Write-Debug "Start $($KeyValue.Extent.StartLineNumber) : $($KeyValue.Extent.StartColumnNumber) (char $($KeyValue.Extent.StartOffset))" + # # Write-Debug "End $($KeyValue.Extent.EndLineNumber) : $($KeyValue.Extent.EndColumnNumber) (char $($KeyValue.Extent.EndOffset))" + + # In PowerShell 5+ we can just use: + if ($KeyValue.SafeGetValue) { + $KeyValue.SafeGetValue() + } else { + # Otherwise, this worked for simple values: + $Expression = $KeyValue.GetPureExpression() + if ($Expression.Value) { + $Expression.Value + } else { + # For complex (arrays, hashtables) we parse it ourselves + ConvertFrom-Metadata $KeyValue + } + } + } + } +} +#EndRegion '.\Public\Get-Metadata.ps1' 97 +#Region '.\Public\Import-Metadata.ps1' 0 +function Import-Metadata { + <# + .Synopsis + Creates a data object from the items in a Metadata file (e.g. a .psd1) + .Description + Serves as a wrapper for ConvertFrom-Metadata to explicitly support importing from files + .Example + $data = Import-Metadata .\Configuration.psd1 -Ordered + + Convert a module manifest into a hashtable of properties for introspection, preserving the order in the file + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "", Justification = "Too late to call it Metadatum, LOL")] + [CmdletBinding()] + param( + # The path to the metadata (.psd1) file to import + [Parameter(ValueFromPipeline = $true, Mandatory = $true, ValueFromPipelineByPropertyName = $true)] + [Alias("PSPath", "Content")] + [string]$Path, + + # A hashtable of MetadataConverters (same as with Add-MetadataConverter) + [Hashtable]$Converters = @{}, + + # If set (and PowerShell version 4 or later) preserve the file order of configuration + # This results in the output being an OrderedDictionary instead of Hashtable + [Switch]$Ordered, + + # Allows extending the valid variables which are allowed to be referenced in metadata + # BEWARE: This exposes the value of these variables in the calling context to the metadata file + # You are reponsible to only allow variables which you know are safe to share + [String[]]$AllowedVariables, + + # You should not pass this. + # The PSVariable parameter is for preserving variable scope within the Metadata commands + [System.Management.Automation.PSVariableIntrinsics]$PSVariable + ) + process { + if (!$PSVariable) { + $PSVariable = $PSCmdlet.SessionState.PSVariable + } + if (Test-Path $Path) { + # Write-Debug "Importing Metadata file from `$Path: $Path" + if (!(Test-Path $Path -PathType Leaf)) { + $Path = Join-Path $Path ((Split-Path $Path -Leaf) + $ModuleManifestExtension) + } + } + if (!(Test-Path $Path)) { + WriteError -ExceptionType System.Management.Automation.ItemNotFoundException ` + -Message "Can't find file $Path" ` + -ErrorId "PathNotFound,Metadata\Import-Metadata" ` + -Category "ObjectNotFound" + return + } + try { + ConvertFrom-Metadata -InputObject $Path -Converters $Converters -Ordered:$Ordered -AllowedVariables $AllowedVariables -PSVariable $PSVariable + } catch { + ThrowError $_ + } + } +} +#EndRegion '.\Public\Import-Metadata.ps1' 60 +#Region '.\Public\Test-PSVersion.ps1' 0 +function Test-PSVersion { + <# + .Synopsis + Test the PowerShell Version + .Description + This function exists so I can do things differently on older versions of PowerShell. + But the reason I test in a function is that I can mock the Version to test the alternative code. + .Example + if(Test-PSVersion -ge 3.0) { + ls | where Length -gt 12mb + } else { + ls | Where { $_.Length -gt 12mb } + } + + This is just a trivial example to show the usage (you wouldn't really bother for a where-object call) + #> + [OutputType([bool])] + [CmdletBinding()] + param( + [Version]$Version = $PSVersionTable.PSVersion, + [Version]$lt, + [Version]$le, + [Version]$gt, + [Version]$ge, + [Version]$eq, + [Version]$ne + ) + + $all = @( + if ($lt) { $Version -lt $lt } + if ($gt) { $Version -gt $gt } + if ($le) { $Version -le $le } + if ($ge) { $Version -ge $ge } + if ($eq) { $Version -eq $eq } + if ($ne) { $Version -ne $ne } + ) + + $all -notcontains $false +} +#EndRegion '.\Public\Test-PSVersion.ps1' 40 +#Region '.\Public\Update-Metadata.ps1' 0 +function Update-Metadata { + <# + .Synopsis + Update a single value in a PowerShell metadata file + .Description + By default Update-Metadata increments "ModuleVersion" + because my primary use of it is during builds, + but you can pass the PropertyName and Value for any key in a module Manifest, its PrivateData, or the PSData in PrivateData. + + NOTE: This will not currently create new keys, or uncomment keys. + .Example + Update-Metadata .\Configuration.psd1 + + Increments the Build part of the ModuleVersion in the Configuration.psd1 file + .Example + Update-Metadata .\Configuration.psd1 -Increment Major + + Increments the Major version part of the ModuleVersion in the Configuration.psd1 file + .Example + Update-Metadata .\Configuration.psd1 -Value '0.4' + + Sets the ModuleVersion in the Configuration.psd1 file to 0.4 + .Example + Update-Metadata .\Configuration.psd1 -Property ReleaseNotes -Value 'Add the awesome Update-Metadata function!' + + Sets the PrivateData.PSData.ReleaseNotes value in the Configuration.psd1 file! + #> + [Alias("Update-Manifest")] + # Because PSSCriptAnalyzer team refuses to listen to reason. See bugs: #194 #283 #521 #608 + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "")] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "", Justification = "Too late to call it Metadatum, LOL")] + [CmdletBinding(SupportsShouldProcess)] + param( + # The path to the module manifest file -- must be a .psd1 file + # As an easter egg, you can pass the CONTENT of a psd1 file instead, and the modified data will pass through + [Parameter(ValueFromPipelineByPropertyName = "True", Position = 0)] + [Alias("PSPath")] + [ValidateScript( { if ([IO.Path]::GetExtension($_) -ne ".psd1") { + throw "Path must point to a .psd1 file" + } $true })] + [string]$Path, + + # The property to be set in the manifest. It must already exist in the file (and not be commented out) + # This searches the Manifest root properties, then the properties PrivateData, then the PSData + [Parameter(ParameterSetName = "Overwrite")] + [string]$PropertyName = 'ModuleVersion', + + # A new value for the property + [Parameter(ParameterSetName = "Overwrite", Mandatory)] + $Value, + + # By default Update-Metadata increments ModuleVersion; this controls which part of the version number is incremented + [Parameter(ParameterSetName = "IncrementVersion")] + [ValidateSet("Major", "Minor", "Build", "Revision")] + [string]$Increment = "Build", + + # When set, and incrementing the ModuleVersion, output the new version number. + [Parameter(ParameterSetName = "IncrementVersion")] + [switch]$Passthru + ) + process { + $KeyValue = Get-Metadata $Path -PropertyName $PropertyName -Passthru + + if ($PSCmdlet.ParameterSetName -eq "IncrementVersion") { + $Version = [Version]$KeyValue.GetPureExpression().Value # SafeGetValue() + + $Version = switch ($Increment) { + "Major" { + [Version]::new($Version.Major + 1, 0) + } + "Minor" { + $Minor = if ($Version.Minor -le 0) { + 1 + } else { + $Version.Minor + 1 + } + [Version]::new($Version.Major, $Minor) + } + "Build" { + $Build = if ($Version.Build -le 0) { + 1 + } else { + $Version.Build + 1 + } + [Version]::new($Version.Major, $Version.Minor, $Build) + } + "Revision" { + $Build = if ($Version.Build -le 0) { + 0 + } else { + $Version.Build + } + $Revision = if ($Version.Revision -le 0) { + 1 + } else { + $Version.Revision + 1 + } + [Version]::new($Version.Major, $Version.Minor, $Build, $Revision) + } + } + + $Value = $Version + + if ($Passthru) { + $Value + } + } + + $Value = ConvertTo-Metadata $Value + + $Extent = $KeyValue.Extent + while ($KeyValue.parent) { + $KeyValue = $KeyValue.parent + } + + $ManifestContent = $KeyValue.Extent.Text.Remove( + $Extent.StartOffset, + ($Extent.EndOffset - $Extent.StartOffset) + ).Insert($Extent.StartOffset, $Value).Trim() + + if (Test-Path $Path) { + Set-Content -Encoding UTF8 -Path $Path -Value $ManifestContent + } else { + $ManifestContent + } + } +} +#EndRegion '.\Public\Update-Metadata.ps1' 128 +#Region '.\Public\Update-Object.ps1' 0 +function Update-Object { + <# + .Synopsis + Recursively updates a hashtable or custom object with new values + .Description + Updates the InputObject with data from the update object, updating or adding values. + .Example + Update-Object -Input @{ + One = "Un" + Two = "Dos" + } -Update @{ + One = "Uno" + Three = "Tres" + } + + Updates the InputObject with the values in the UpdateObject, + will return the following object: + + @{ + One = "Uno" + Two = "Dos" + Three = "Tres" + } + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "")] # Because PSSCriptAnalyzer team refuses to listen to reason. See bugs: #194 #283 #521 #608 + [CmdletBinding(SupportsShouldProcess)] + param( + # The object (or hashtable) with properties (or keys) to overwrite the InputObject + [AllowNull()] + [Parameter(Position = 0, Mandatory = $true)] + $UpdateObject, + + # This base object (or hashtable) will be updated and overwritten by the UpdateObject + [Parameter(ValueFromPipeline = $true, Mandatory = $true)] + $InputObject, + + # A list of values which (if found on InputObject) should not be updated from UpdateObject + [Parameter()] + [string[]]$ImportantInputProperties + ) + process { + # Write-Debug "INPUT OBJECT:" + # Write-Debug (($InputObject | Out-String -Stream | ForEach-Object TrimEnd) -join "`n") + # Write-Debug "Update OBJECT:" + # Write-Debug (($UpdateObject | Out-String -Stream | ForEach-Object TrimEnd) -join "`n") + if ($Null -eq $InputObject) { + return + } + + # $InputObject -is [PSCustomObject] -or + if ($InputObject -is [System.Collections.IDictionary]) { + $OutputObject = $InputObject + } else { + # Create a PSCustomObject with all the properties + $OutputObject = [PSObject]$InputObject # | Select-Object * | % { } + } + + if (!$UpdateObject) { + $OutputObject + return + } + + if ($UpdateObject -is [System.Collections.IDictionary]) { + $Keys = $UpdateObject.Keys + } else { + $Keys = @($UpdateObject | + Get-Member -MemberType Properties | + Where-Object { $p1 -notcontains $_.Name } | + Select-Object -ExpandProperty Name) + } + + function TestKey { + [OutputType([bool])] + [CmdletBinding()] + param($InputObject, $Key) + [bool]$( + if ($InputObject -is [System.Collections.IDictionary]) { + $InputObject.ContainsKey($Key) + } else { + Get-Member -InputObject $InputObject -Name $Key + } + ) + } + + # # Write-Debug "Keys: $Keys" + foreach ($key in $Keys) { + if ($key -notin $ImportantInputProperties -or -not (TestKey -InputObject $InputObject -Key $Key) ) { + # recurse Dictionaries (hashtables) and PSObjects + if (($OutputObject.$Key -is [System.Collections.IDictionary] -or $OutputObject.$Key -is [PSObject]) -and + ($InputObject.$Key -is [System.Collections.IDictionary] -or $InputObject.$Key -is [PSObject])) { + $Value = Update-Object -InputObject $InputObject.$Key -UpdateObject $UpdateObject.$Key + } else { + $Value = $UpdateObject.$Key + } + + if ($OutputObject -is [System.Collections.IDictionary]) { + $OutputObject.$key = $Value + } else { + $OutputObject = Add-Member -InputObject $OutputObject -MemberType NoteProperty -Name $key -Value $Value -PassThru -Force + } + } + } + + $OutputObject + } +} +#EndRegion '.\Public\Update-Object.ps1' 107 +#Region '.\Footer\InitialMetadataConverters.ps1' 0 +$MetadataSerializers = @{} +$MetadataDeserializers = @{} + +if ($Converters -is [Collections.IDictionary]) { + Add-MetadataConverter $Converters +} +function PSCredentialMetadataConverter { + <# + .Synopsis + Creates a new PSCredential with the specified properties + .Description + This is just a wrapper for the PSObject constructor with -Property $Value + It exists purely for the sake of psd1 serialization + .Parameter Value + The hashtable of properties to add to the created objects + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "EncodedPassword")] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingUserNameAndPasswordParams", "")] + param( + # The UserName for this credential + [string]$UserName, + # The Password for this credential, encoded via ConvertFrom-SecureString + [string]$EncodedPassword + ) + New-Object PSCredential $UserName, (ConvertTo-SecureString $EncodedPassword) +} + +# The OriginalMetadataSerializers +Add-MetadataConverter @{ + [bool] = { if ($_) { '$True' } else { '$False' } } + [Version] = { "'$_'" } + [PSCredential] = { 'PSCredential "{0}" "{1}"' -f $_.UserName, (ConvertFrom-SecureString $_.Password) } + [SecureString] = { "ConvertTo-SecureString {0}" -f (ConvertFrom-SecureString $_) } + [Guid] = { "Guid '$_'" } + [DateTime] = { "DateTime '{0}'" -f $InputObject.ToString('o') } + [DateTimeOffset] = { "DateTimeOffset '{0}'" -f $InputObject.ToString('o') } + [ConsoleColor] = { "ConsoleColor {0}" -f $InputObject.ToString() } + + [System.Management.Automation.SwitchParameter] = { if ($_) { '$True' } else { '$False' } } + # This GUID is here instead of as a function + # just to make sure the tests can validate the converter hashtables + "Guid" = { [Guid]$Args[0] } + "DateTime" = { [DateTime]$Args[0] } + "DateTimeOffset" = { [DateTimeOffset]$Args[0] } + "ConsoleColor" = { [ConsoleColor]$Args[0] } + "ScriptBlock" = { [scriptblock]::Create($Args[0]) } + "PSCredential" = (Get-Command PSCredentialMetadataConverter).ScriptBlock + "FromPsMetadata" = { + $TypeName, $Args = $Args + # Can't construct a PowerLine.Cap with ([Type]$TypeName)::new() + $Output = New-Object $TypeName + $Output.FromPsMetadata($Args) + $Output + } + "PSObject" = { param([hashtable]$Properties, [string[]]$TypeName) + $Result = New-Object System.Management.Automation.PSObject -Property $Properties + $TypeName += @($Result.PSTypeNames) + $Result.PSTypeNames.Clear() + foreach ($Name in $TypeName) { + $Result.PSTypeNames.Add($Name) + } + $Result } + +} + +$Script:OriginalMetadataSerializers = $script:MetadataSerializers.Clone() +$Script:OriginalMetadataDeserializers = $script:MetadataDeserializers.Clone() + +Export-ModuleMember -Function *-* -Alias * +#EndRegion '.\Footer\InitialMetadataConverters.ps1' 70 diff --git a/Tools/Metadata/1.5.7/PSGetModuleInfo.xml b/Tools/Metadata/1.5.7/PSGetModuleInfo.xml new file mode 100644 index 000000000000..9adb1fa26d89 --- /dev/null +++ b/Tools/Metadata/1.5.7/PSGetModuleInfo.xml @@ -0,0 +1,154 @@ + + + + Microsoft.PowerShell.Commands.PSRepositoryItemInfo + System.Management.Automation.PSCustomObject + System.Object + + + Metadata + 1.5.7 + Module + A module for PowerShell data serialization + Joel Bennett + Jaykul + Copyright (c) 2014-2021 by Joel Bennett, all rights reserved. +
2022-08-17T05:03:00+08:00
+ +
2025-12-08T22:53:40.2167412+08:00
+ + + + Microsoft.PowerShell.Commands.DisplayHintType + System.Enum + System.ValueType + System.Object + + DateTime + 2 + + +
+ + http://opensource.org/licenses/MIT + https://github.com/PoshCode/Metadata + + + + System.Object[] + System.Array + System.Object + + + Serialization + Metadata + Development + Configuration + Settings + PSModule + + + + + System.Collections.Hashtable + System.Object + + + + Workflow + + + + + + + Cmdlet + + + + Command + + + + Add-MetadataConverter + ConvertFrom-Metadata + ConvertTo-Metadata + Export-Metadata + Get-Metadata + Import-Metadata + Test-PSVersion + Update-Metadata + Update-Object + + + + + Function + + + + Add-MetadataConverter + ConvertFrom-Metadata + ConvertTo-Metadata + Export-Metadata + Get-Metadata + Import-Metadata + Test-PSVersion + Update-Metadata + Update-Object + + + + + DscResource + + + + RoleCapability + + + + + + Fixes PSObject serialization bug + + + + + https://www.powershellgallery.com/api/v2 + PSGallery + NuGet + + + System.Management.Automation.PSCustomObject + System.Object + + + Copyright (c) 2014-2021 by Joel Bennett, all rights reserved. + A module for PowerShell data serialization + False + Fixes PSObject serialization bug + True + True + 697777 + 780046 + 15204 + 17/08/2022 5:03:00 AM +08:00 + 17/08/2022 5:03:00 AM +08:00 + 8/12/2025 2:50:00 PM +08:00 + Serialization Metadata Development Configuration Settings PSModule PSFunction_Add-MetadataConverter PSCommand_Add-MetadataConverter PSFunction_ConvertFrom-Metadata PSCommand_ConvertFrom-Metadata PSFunction_ConvertTo-Metadata PSCommand_ConvertTo-Metadata PSFunction_Export-Metadata PSCommand_Export-Metadata PSFunction_Get-Metadata PSCommand_Get-Metadata PSFunction_Import-Metadata PSCommand_Import-Metadata PSFunction_Test-PSVersion PSCommand_Test-PSVersion PSFunction_Update-Metadata PSCommand_Update-Metadata PSFunction_Update-Object PSCommand_Update-Object PSIncludes_Function + False + 2025-12-08T14:50:00Z + 1.5.7 + Joel Bennett + false + Module + Metadata.nuspec|Metadata.psd1|Metadata.psm1 + c7505d40-646d-46b5-a440-8a81791c5d23 + HuddledMasses.org + + + C:\Users\Zac\Documents\PowerShell\Modules\Metadata\1.5.7 +
+
+
From d3d989239922033360844a522ef1c930b4949c9e Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Sun, 7 Jun 2026 14:56:27 +0800 Subject: [PATCH 162/202] cache middleware changes --- .../HTTP Functions/New-CippCoreRequest.ps1 | 2 +- .../Start-CIPPDBTestsRun.ps1 | 2 + .../Start-UserTasksOrchestrator.ps1 | 2 + .../Timer Functions/Start-DurableCleanup.ps1 | 4 + Shared/CIPPSharp/CIPPTestDataCache.cs | 84 ++++++++++++++---- Shared/CIPPSharp/bin/CIPPSharp.dll | Bin 41984 -> 42496 bytes 6 files changed, 76 insertions(+), 18 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/New-CippCoreRequest.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/New-CippCoreRequest.ps1 index 985e6026d4a8..97dd96f5bf7d 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/New-CippCoreRequest.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/New-CippCoreRequest.ps1 @@ -3,7 +3,7 @@ using namespace Microsoft.Azure.Functions.PowerShellWorker function New-CippCoreRequest { <# .SYNOPSIS - Main entrypoint for all HTTP triggered functions in CIPP + Main entrypoint for all HTTP triggered functions in CIPP, this must live in the CIPPCore module .DESCRIPTION This function is the main entry point for all HTTP triggered functions in CIPP. It routes requests to the appropriate function based on the CIPPEndpoint parameter in the request. .FUNCTIONALITY diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPDBTestsRun.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPDBTestsRun.ps1 index 00585cdbf107..4e08a43c16c1 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPDBTestsRun.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPDBTestsRun.ps1 @@ -38,6 +38,8 @@ function Start-CIPPDBTestsRun { } try { + try { [CIPP.TestDataCache]::Clear() } catch { Write-Information "TestDataCache clear skipped: $($_.Exception.Message)" } + $AllTenantsList = if ($TenantFilter -eq 'allTenants') { $DbCounts = Get-CIPPDbItem -CountsOnly -TenantFilter 'allTenants' $TenantsWithData = $DbCounts | Where-Object { (($_.DataCount ?? $_.Count) ?? 0) -gt 0 } | Select-Object -ExpandProperty PartitionKey -Unique diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UserTasksOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UserTasksOrchestrator.ps1 index c2895777feed..da1a515e285b 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UserTasksOrchestrator.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UserTasksOrchestrator.ps1 @@ -11,6 +11,8 @@ function Start-UserTasksOrchestrator { $TaskId = $null ) + try { [CIPP.TestDataCache]::ClearExpired() } catch { Write-Information "TestDataCache clearexpired skipped: $($_.Exception.Message)" } + $Table = Get-CippTable -tablename 'ScheduledTasks' if ($TaskId) { diff --git a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-DurableCleanup.ps1 b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-DurableCleanup.ps1 index 5987188d5a10..edc12c427324 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-DurableCleanup.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-DurableCleanup.ps1 @@ -18,6 +18,10 @@ function Start-DurableCleanup { [int]$MaxDuration = 86400 ) + if $env:CIPPNG -eq 'true' { + return + } + $WarningPreference = 'SilentlyContinue' $TargetTime = (Get-Date).ToUniversalTime().AddSeconds(-$MaxDuration) $Context = New-AzDataTableContext -ConnectionString $env:AzureWebJobsStorage diff --git a/Shared/CIPPSharp/CIPPTestDataCache.cs b/Shared/CIPPSharp/CIPPTestDataCache.cs index 282ebee87b42..d92db61483e7 100644 --- a/Shared/CIPPSharp/CIPPTestDataCache.cs +++ b/Shared/CIPPSharp/CIPPTestDataCache.cs @@ -6,15 +6,17 @@ using System.Reflection; using System.Text.Json; using System.Threading; +using System.Threading.Tasks; namespace CIPP { /// - /// Process-scoped, thread-safe LRU cache for test data lookups. - /// Shared across all PowerShell runspaces. Bounded by both a byte-size - /// cap (default 50 MB) and a short TTL (default 1 minute) so that test - /// suites running against a single tenant get fast cache hits without - /// accumulating stale Gen2 roots that cause GC thrashing. + /// Host-scoped, thread-safe LRU cache for test data lookups. The DLL is + /// loaded once per Azure Functions host, so every PowerShell worker + /// process on that host shares this exact instance. Bounded by both a + /// byte-size cap (default 100 MB) and a short TTL (default 5 minutes) + /// so that test suites running against a single tenant get fast cache + /// hits without accumulating stale Gen2 roots that cause GC thrashing. /// public static class TestDataCache { @@ -28,10 +30,12 @@ public static class TestDataCache private static readonly Dictionary> _lruIndex = new(); // key → node private static readonly object _lruLock = new(); private static long _currentBytes; - private static int _accessCount; + private static long _accessCount; private static long _hits; private static long _misses; private static long _evictions; + private static long _oversized; + private static int _sweepInFlight; // 0 = idle, 1 = a background ClearExpired is running private sealed class CacheEntry { @@ -60,7 +64,11 @@ public static void Configure(long maxBytes = 100L * 1024 * 1024, int ttlSeconds public static bool TryGet(string key, out object? value) { - Interlocked.Increment(ref _accessCount); + var count = Interlocked.Increment(ref _accessCount); + // Every ~1000 accesses, kick off a background sweep so TTL-expired + // entries that nobody re-reads still get evicted. CAS-guarded so + // overlapping triggers collapse to a single sweep. + if ((count % 1000) == 0) TryFireBackgroundSweep(); if (_cache.TryGetValue(key, out var entry) && !entry.IsExpired) { @@ -95,9 +103,14 @@ public static void Set(string key, object? value) int itemCount = value is ICollection col ? col.Count : 0; long sizeBytes = EstimateValueSize(value, itemCount); - // If a single entry exceeds the cap, don't cache it at all + // If a single entry exceeds the cap, don't cache it at all — bump + // _oversized so GetDiagnostics surfaces these silent drops instead + // of leaving callers to chase phantom misses. if (sizeBytes > _maxBytes) + { + Interlocked.Increment(ref _oversized); return; + } // Remove existing entry for this key first if (_cache.ContainsKey(key)) @@ -170,6 +183,7 @@ public static void Clear() Interlocked.Exchange(ref _hits, 0); Interlocked.Exchange(ref _misses, 0); Interlocked.Exchange(ref _evictions, 0); + Interlocked.Exchange(ref _oversized, 0); } /// @@ -191,6 +205,40 @@ public static int ClearTenant(string tenantFilter) return removed; } + /// + /// Remove every entry whose TTL has elapsed. Pair to the lazy per-key + /// eviction in TryGet — handles keys that nobody reads again. Safe to + /// call from anywhere; the background sweep triggered by TryGet uses + /// this method. + /// + public static int ClearExpired() + { + // Snapshot first; RemoveEntry mutates _cache and _lruIndex under _lruLock. + var expiredKeys = new List(); + foreach (var kvp in _cache) + { + if (kvp.Value.IsExpired) expiredKeys.Add(kvp.Key); + } + foreach (var key in expiredKeys) RemoveEntry(key); + return expiredKeys.Count; + } + + /// + /// Fire-and-forget a single background ClearExpired sweep. The CAS guard + /// collapses overlapping triggers so we never have more than one sweep + /// running at a time, regardless of read pressure. + /// + private static void TryFireBackgroundSweep() + { + if (Interlocked.CompareExchange(ref _sweepInFlight, 1, 0) != 0) return; + Task.Run(() => + { + try { ClearExpired(); } + catch { /* swallow — sweep is best-effort */ } + finally { Interlocked.Exchange(ref _sweepInFlight, 0); } + }); + } + public static int Count => _cache.Count; public static long CurrentBytes => Interlocked.Read(ref _currentBytes); public static double CurrentMB => Math.Round(Interlocked.Read(ref _currentBytes) / (1024.0 * 1024.0), 2); @@ -199,6 +247,7 @@ public static int ClearTenant(string tenantFilter) public static long Hits => Interlocked.Read(ref _hits); public static long Misses => Interlocked.Read(ref _misses); public static long Evictions => Interlocked.Read(ref _evictions); + public static long Oversized => Interlocked.Read(ref _oversized); public static double HitRate => (_hits + _misses) > 0 ? Math.Round(_hits * 100.0 / (_hits + _misses), 1) : 0; @@ -326,12 +375,16 @@ private static long EstimateValueSize(object? value, int itemCount) /// public static CacheDiagnostics GetDiagnostics() { - var now = DateTime.UtcNow; var entries = _cache.ToArray(); // snapshot long totalBytes = 0; var byType = new Dictionary(); + int active = 0, expired = 0; + DateTime? earliestExpiry = null, latestExpiry = null; + // Single pass — use the SizeBytes stored at insert instead of + // re-running EstimateValueSize (which would JSON-serialize every + // PSObject tree on every diagnostic poll and thrash the LOH). foreach (var kvp in entries) { var parts = kvp.Key.Split('|', 2); @@ -341,7 +394,7 @@ public static CacheDiagnostics GetDiagnostics() if (kvp.Value.Value is ICollection col) itemCount = col.Count; - long entryBytes = EstimateValueSize(kvp.Value.Value, itemCount); + long entryBytes = kvp.Value.SizeBytes; totalBytes += entryBytes; if (!byType.TryGetValue(dataType, out var bucket)) @@ -352,12 +405,7 @@ public static CacheDiagnostics GetDiagnostics() bucket.EntryCount++; bucket.TotalBytes += entryBytes; bucket.TotalItems += itemCount; - } - int active = 0, expired = 0; - DateTime? earliestExpiry = null, latestExpiry = null; - foreach (var kvp in entries) - { if (kvp.Value.IsExpired) { expired++; } else { active++; } var exp = kvp.Value.ExpiresUtc; if (earliestExpiry == null || exp < earliestExpiry) earliestExpiry = exp; @@ -377,9 +425,10 @@ public static CacheDiagnostics GetDiagnostics() Misses = Interlocked.Read(ref _misses), HitRate = HitRate, Evictions = Interlocked.Read(ref _evictions), + Oversized = Interlocked.Read(ref _oversized), EarliestExpiryUtc = earliestExpiry, LatestExpiryUtc = latestExpiry, - AccessCount = _accessCount, + AccessCount = Interlocked.Read(ref _accessCount), TypeBreakdown = byType.Values .OrderByDescending(b => b.TotalBytes) .ToList(), @@ -401,9 +450,10 @@ public class CacheDiagnostics public long Misses { get; set; } public double HitRate { get; set; } public long Evictions { get; set; } + public long Oversized { get; set; } public DateTime? EarliestExpiryUtc { get; set; } public DateTime? LatestExpiryUtc { get; set; } - public int AccessCount { get; set; } + public long AccessCount { get; set; } public List TypeBreakdown { get; set; } = new(); } diff --git a/Shared/CIPPSharp/bin/CIPPSharp.dll b/Shared/CIPPSharp/bin/CIPPSharp.dll index a6afa3419dd6982034d9bd1526f591ede1bb183a..b5ad6c396c0c75f6e7a2b10812f9de152a0d1e58 100644 GIT binary patch delta 18042 zcmcJ0dtg-6wf8!EX3o4alX+#5$$K&ohCC2RK%yW-5X3@JzzPyg!b2Vc0|_V)l1U&y z5m3V*f`SsoR$FZ4qFz9)iK4X@E7nKF2PHnL{nV@8YORmr_gm{69`W}3?jQI2qVwD9 zx7ONguf5OS`<%09Xnjt#9#yygv}pOKL)MY`can+<-P%B+yP(hwV#GlIS5Do zyA&TqHrh9Z=<)ePTB>K)`3Ejn_owCGKx7S2M)PthR~t005tRmL^$iEY=@!eS*TB}d zJhQl^2Er-*cI4SOoygUO!T;JRIPIjIliLxbu^j-amOAfd60ps8^d;9M?8+>M} zBzW1(m9Pg$H8)9VXF;AGWm2>utX0Y+Lgv?{s5)QCcv6&dv7T)D;u@ZH#O)}OElYb>_$61s7~m4HCGLPz+{uxPL0cMnd+%d! z^bg)_iM!abvA55I?1P@bhb=A3&NX{a>Azw-Isp5!16#r+nl>(VF!K%VkyI*chUWmR zi7|k_dg77Pw^@UW4l%JL9_GTDc$AAcCaudApg3l+D{(M2DRK*{|2fi-({Z}7TOpj% zABT72CTXkVz{2c#T9P-f0Fggg1&27+hpX^IOuGfwl-uId;oVXRy$VnQ`+=+MT0QX; zl;uBZIUj0wIL#hV@tv@=3>7y^0_)sW7H$~=>P%oAyG(Hx#1k$@nQo8ohWJ+U0(it* zFubl}i=KEQ_2b-VQuFb4cRrnW9bqJiat2zdULy+jO0d|nd^om3x4qmFF4gS`biw7a z=y9wqJTRME8tqQCym7w2Vb#jIkd+shjfhF2yo&l@&`n4zmFs zbd|uea0Q*l1@Su*SOKmRX?|Oo-ETF9z62dGMmj<7Pfh(tUd~S>>8TP-@0Iia^)ef| znU`56fRX>{>zu01k4)Q$)`GP16)TMuZ)jNyruz;ZKYNCHdA=wSj222;gert9WZ z;js)ah;M_wSmr5{)QkD0KDo-LP3TwZ+x+}%e~U49sW|p{SD6Zz=&B)VrzLT$GgmX_ zsx|DE36c0U6#6OsIk-1I1Uc^IwVn=0VAZ+ON`08z53_-1j6HLj8~l;fjRpOaIRh18 ztxy;pYkU+gE+gc>Lv)uG*%Jpj0Nf9ulqFN<(LoP0eGFhvU|d|~uSq}ciJL&)(xq8X zXoJ0OKaG9l|O78SXqLDK;Or#Giy%Tr$AuO&7->AhRNljl@;bV9Kek z0$&IV##NFn%dKQ^QWnxvsrL%{TLvUYq^yM(C&d9d@L=JZP$tiUQD}wp7>V-S*&uq` z+gM>l@uv|waRiVS_LMGnIPVBr{nmzRT*U+uc42YaLh)xIl~48CTB{MtZ)@1XX0+pK zy_gl&s~YY-Q$hFZp*SwIt89Q@Z}|3%=VRW#y#0h$Ffg0>Uc3O=bzNP6UXBxj!Egy$8$|tJjSdls9m-J31d?p!7<}@>%RI+G9eKybJ zV~FW$;o%F#UxHko-NGYe5>E?{jzP+lULuYv@kcmH*Rdf(Yd5<+#%xIay5nQ-7wAEYA%ugg6n=<$2XoPLkoGlFSw7d!9LL9gBK zo#uA<-GTJPpU`f3q;VVcef{plX11rpfwv{s7n;Fvs6yA7{Yk_r>3W7|_qHrJt#l{6V$1jcU4C&F?@O4!^^1zj^-1U8lbP zz7abS9yM;BpSYd{XJQc-erqDZrF~(H*C)}y>KR#aUf+qOtdG|K6OAm&esI<;dD?F| z8_it;_2wsT6ootm@ZOSGDf~iA5ZP-IP0Zu7fC=p6s5|_O3ELm|h%>y6x!;ypE2W-T zC-y3~kCfOOSm8*VKszlvpq6KLV*|u4qBs5-hQ6(Ai3j)|5afFRHfX&hHM6*Uz)6@} zd15y9ur!3FcUZy-vo>NYaJ8O)MjDy=b8%1V<$e{(%T(jLur>b#HWg>Ll6=*46^TE1bSYMR9{g0(^398;dY`&AQI`XNY_vs0vOQ zfhA)6^VF>+7bZElCH|jqK(hSo+F&Qjmt0rCgj>S|J#%{IXzXefAByW@~(2K1EEWnpS~@Pif6- zP=aD%SydC0`1#188LI{%7@NL8g)~$Ck}DhPze9{;Iq9gq8h(vmu}uOsPKDU`4?uG} zmZg=XgI6nba4uWE26$8xR3;gfi9?*4#%GLEsNN-1FnEO{vC+l6}0=^L5FH; zN0r1sS%vLAaSB4Y%PI2rkWrU3s2)2J(8K3>a90SALi>0&g7GcY)PmYo>6j|2iPMsY z!E!`pQfh0+#5xJ4`O^w6%t~m8(>Hua&=XgXg5Jz{n3unmNqT8LwtO3lKEJQwDpbo! z=%8ieu|2y*5x28B;CGk%0SRXQ!1ROzWn@b|=2PEeGdKk&6bTpm;;s>Qr@kobm*h?w z$@;y1Ph%RpcmQViz0A=ISHpaMLh(T={+A|ynv77f^Yq}ZICpT}0hckj?qCoD95O-# zGyKVn1UFG0@n`UD+9e~<=!dFrF#InQ@dy8P7XF|=WTaqTnG05b|NZypT%5DlMm4pf z*WrPdP-zmYn90KU?P$F*fQFl~dGjn95Yw|R?7~99oMTnraz}AI2!~QzJj8zMO~1>{ zxuTj_lgNNAoCpJhkw7G#$wF2jE1m_Ryjo>qX&cKbt9c`zJ}Bd0CriaMd0pLa;VFZy z-x|zPtGPUtF&4SYo*>IAD<0vPnaZEpyaDzT!QyjPO#DJDlT+D9nr+RPjp$6)fCf{M z$VT1fP0)%%(wfMDqHML{F2w9dF7)9<9ye7GD2V5?kRQm87eGKivd;7)zZp*_r@N6a z_nFeS-@yV@O>79}tJPed${63P8~O1YRSbJpM0%iV&#C2vqdLK*=`l znCw(_=M1qK6Gxp`COnk!3oz#8KR!oaUR^LSv#~#C8Vkf4)?)F*OE}q>StA?S24e9t z=&%|jVZJhrqK_d8wL-_U= zl6h?`jB)-x@k%6gqWXUj#2z1VVDjJ$Z93n=Eb*a;$4^3df;OInM1TU%(S||wEJ$^oCI-v0gx3Qh080!nScYdeEl$XEwG}wzzT<$AA;gQ$vaC{gl z7Qa;jGryekpTj{Gypd~InB;bohjsZDd1W{g^#Y4(+KFb2TTDr6R%PE2yGn^-xSKi3 zh_d=Xp0ccA{^GfK*2Z@Sc%pJRp}TSl@nIc5LdC{iIerYvVc_klXDa)RxE>5woDi9w z!)G@>oCR_eJ+?F4O65U?+$gK@sl&&L&xBO}A$iFK*l_tzr^$h3**ZNT81X+8NvHi- zN;+-!ZwTtNH!veZ$8jR#6Mn`w1RF$iwa5ih_mq$0TvD(pq|;lzhax(SL^Pf56D;?$ zbrU>Ybcba`(yr6-f@RrW3Pqm}2WV?(S};tvS{{q)w8Y0DmP&={(4t(O&X)#Ct*qan zIM8K^^?gLM%EJ0K%Lv5Gle)i2XP+y8x05x(o%vG+`z*>@mOX{mLzC30CHP5^PCrv4 z>|XlB`+V3--y;<-9fz#bKs58DS=j?0ow`w3r&?+LD(^Im&6`MCr%v=ZK=)^`{9I^L z$VCss-$kEmTx)fh$81qXMV^ZeL;saDuP5nnj15)k6?r<{7-765opV%*aihr9$l68g zr1^V;tiL8}Q^-s2WIZ3wrrPWc1z!3UWDIx_H~)e7j1F%K<>BFgyk!w$xqN5ou1Cy6q-USGk6$3LULXj zETeU0{%d(&8Vb~Dnl!K^pGWBC7}uJQ4tVKv8PSWy^G->6r~b433)#gsTWn68kFn?HVsqmxN(0L&>}#*W>2i$Pz_v%3 zWzhh!6^pGOmP3+_`RQyd!8dZue8L9PWHjodrz30|M+<~)FJN{R;nsxg#v#!vnnAab zCF-LA$f=5Im?6qbNNo!(r(X-}iwPcIfu0y?9NpNQNpI7~GAM6MBeLv&KoJ%~y|Q-Hy|tAHzV7;6Mm`7Fmozc;%U z4b)3BZ@9V1*F1A!9h8stz zLDZ484j~+|Ex^jq9jM?Dr;$Zm`P`uQRF(w2jF(#?Q^)slJ3LX%w5c6X6PosTljK6TRPo`+bx>=*# z4*>fL?iPJ0?Eo~}Fg6+;_HZ+Qmb%{}T7~wW{v%mSW=RwP} zu1bs2*(nyKFEPA4bZe0B8d?wQ)9yPk8INFiqx2Wot-HS;9NnW~J{;)*0M@ z+xLPf;cvaenm19|sKI0la`J69T4l0HIloG?(HfH_^ACXCWU@%XL9ne+JO^`_ZP`zN z-eDM#%r_Pr9TR3G9y_bhBYZrnAt*Tf&S?BlKJ`Cu8(J zo3`~c7$=%de~&X;?`XovQ!hO-;chLmPh?fRvwPo@@bIChJ{~F!yVmZ-{keA70|^d^X7j9_9KI#6FL>rLSY^B zo5Fvj71CQK`$pIVeuAZB2fZcqaiJZwSXduCnG$tSL6j?QGud{rS@E>Wwuxd}X0m0% z-aF0g|3tmQa>H|ORCx-i{XBzhF1p(qBkMq6NxC#6-P4B_8kF3H8J=RY4KiG3MRGm; zsZ!W>3VNfS5~?%VG<$_-Fr75nE0OV@3uu2O*V|6-Mka!NCafdtc461z{f948mN#a* zfUYxHvdH^#+69y_>3?QypbP0nli9;>q+LjSCd?Hhv2N>d`l-oEV^?^FQ-{H1xJS@l zVcV%QHqA4F1`g$V+v$yPz2{=G;yjL7lW(4949zxKNivx08AsbpTH|=edMQ;7GhD5q zgy%B)smXkyT+akLYBKKcL>g6PcyfOy(w)ML{!XOFOk108Ic&#F)>_cynM9lMR?W$5 zEl95MTtR8WnRd|0SgvOZZ5FnjCTC6bOr^>ZhU?72pMpJSvTDZ`&y}>~BBS1|;fY{B z6Sm#qE@Ioine4lq4zS;t>?!>=us2PXliQI5eaEDk*!61noZ981j6{bAcY+mG8*F3vZcp+mdd{RH3hwhvr@a?T z6ZA^tAx{lWyF^@N3f0gf!qz+b=T?yT$`j1O}az>Sg)neO!lDuh^LmmHdzB~b)=2uWY#;@!B$6jS`&5*wt5Pf z>0Xl+6m?q{&@WAPUeRIC0(#hF)55QL zuBXRMwjg{G>=}c}f?Y_jk4+lQ|1*69Eu@pCtM2oG{nca>iYD6@(dQfCc92k-c9te$^M9BHq)0T zJBVbq&?%FRw|cxcQ}9y06l-a>HP3qs<(ljhYutM)4KvwNTZMNkjS-gY(cZ(`e~ONd z=ZZbrOX-8b-ZB~6w$Z%FXKmZ)24U+Re6P5TT1>|GirZ*|$@pHeoo+E1-z&D$4wL-| zw%cis!IF*|7;mQs3@TT92R$y#m$mww6ZW z6kr!^HrX#}igy=nJI8Z3?J{lSwU3;;5nZF#9DF+UA|I7eG{`N={MRU+$A;z{!kc|P z&`LY<2|ta^E8xS19XN(MQ$Yglz{98_AFRFg7oo1A4^h`b+a=C1iSq#t>U6pVXs6Gi zanMzexvs2#PQ(cGe?sW~YZ_po|JXi=4Ig&t>ZnxipW)6e1W&7}3tk zcuAF7tWks$Co@q+Jv;-9@z>~6F|(?7Ka4>C|1f!O=n^~U_!8cGH2R;&J$xi9e;ToT zF8)BDJY)WW;lI_N8`O~hWdzIsSOw4Rsv(!yG5*GYD|$ti->)$S8kGr-38mwy(1(wc zI&hTi2kJN)?})glKhQ@wJpon;Rtt_7yh3ogU_DTy`9K`b0yAi%$Q{6(Bu=wX&`ZEV z>H!v`xsFH~MP*}-%7&aK=UZuVzO@uq9ltvKq~IE0d+uEPvVi4WkwbVvy%93wQh7bC zQbP-Frb;;#t0X=Zt0X=ZtCUl*WThO7Rm!p0a0xVA0*w)QjK~v2o*?p6k*A70T$&jp zI6-i#;A%NuTaDwjvD7Z7Ywg6RYwdEn){Y-QJP4Uj*YLeMAFs6&AFs8`@!Do-V6!x^ zP2_DNZF_S~1X$02{G9iiX(oa(5^uZa8>Wk*J+_h>}nMD;Pf>Z=8E%#UeAXtDZ2JXfk^ zv32S@>PRBQO7(Q?kXotc2iK{MYLo9v$ctj%0xQ$=ELM%1S*a!zOth?2n`0|1X`%@U z<_h)!eq$*ER#|t-cNbb3Yb5Sm{NO-E#n^%Wq_|EtuV5cltj zLPlrC7HgFnkoYpv8Wg8ydSuYFzg81RcoxAl;+#-4$`AM};lnot#RZRGFP z;o7gG6V({)%|O0wjJ7j2Q90FnMKPOSJ%CMnf+RXs>+2Y1J48m| ziXOLhs}Bo)YwM9Xv$bEuKCngA*F~QIcV+wyKKB%TZ(EH-C#vLZ>BoLG$gv)QN`fE2 zybn)3v$X*ZpU&a3^o7#P*%}YyGQov{RqDKgA^L2sxZpy)UHZ3DE%!~-SE|)T*Xx@x zD((6<8I^5V%rV<$_;6$%tzDwuo0Q?&E5o%}8{{~KYQ{)V`fGZBT+8pY<1{sV0?n}g zkSg>3Meo-1?2q-A)K?+R{*vm-w%fnf#`(PV-_ggh5HPJzF7O|a_eqnQS{u2@?o_{y zj)8_-*(cFDRdRpMrEu-%YqUQuuE({HW83Y=wXEPCX!z>R);7ofZa*T{D)n+f$Z}qwqYYN7a3hk7$E)qnKBmEzcUkaUaR#~Rmt*$=mNI@=s{GA;c=&Dpa29=7pnx$Hk_j1=# zGull1Ph7*rbBuUS5YGwX$@}IS>r%AX>Pl*swUPC%sgiq*;#FItcm?kx?(tIXejFEd zAeGhD4sC1*_d;#2jzBExa74u_%66^azmzN&I7smnY$^qabf`@<#-vVX29n?(`;4_r( z_TmkYJzo-^R|SuY=iB1hBcAWW(?K3B1`KH$FiX>cQ7r)MtHpukTDjXr$syWckSgs$ zoMm56-=+7ZCb~bZAHLOE7)=?wmdeP4keJeF4p=uM=T2ZybwZ^wcTsuX7ujso((=D2( z@qSn1ds>3_h~{0^!D+`lf-bc^O`(VP@bk7(W%4JodrDM?OoE&Tk2YLdRFsPN+z zWLhoqXffAGg?f=^skQX?^jYH5D*86juNB-Qnog1L73>mCx5zz$nxzR5qJq_eV}Lcj zWWA6!!A`+$!IKuww?}0BDvPL^ONso9Jk|)xp+62R9HE zxsTu|(Nv3ECvv^Wts=LHyvLE`YF(o05!9R#NU&P4Ua(EDQ?OgGM^JOIztttfD{{5q z7}3;=+$Pv5*e%#2sJUnH{ExahL?8E^wAfcIa+{lHLnkygU$kyG8C1tLBllAUFy~ecRHjJ$F*l=c^M^FNRi;+eF?YawnX8zAllw zMSoJ{9+An*fiy3NE)+Q`@+gt3MXnRM-g_s0l+h}3o9OqjoJ{W$)k#50=WvCB(R2nM?{1?vUd1Um)0rB;ui7UT#~!9BsXkh=s= z3Q{P^s=^TCD8V|xR>3`jU4kbCDMS1PM+w#mwhHbM>=L9f?-`>6>%!cbR>3`jU4kdW zv&vwgOg8Tk>=HaFNLj4!%Hk#|0!)26tvvInt&iSczfJ#4kJ;<&P4@fja~;<^?oJJw zUXbjIFC&NGHT44AviO^d*BsMv4D+J*T3|en<>nlg>vLuScjV6l<`pbrIlF~m2L%OB>myj}c{iT?+=?0-ux`+uH)Bk-!+)xbx*ZAlb|^Vb0_MH_*`!z>r)Z(;PZ z><+X3yx8r)*+q8(Dfe#Rv--Wj0+CCyS^r@^>$m4H?#N^O!ok?>W6!FbHegur^K6#) z>p#aiEp0?WbV9I13?JrmW7~5WyM1ikk;n2E4#rpPzXYBVtSDm5;_#!uB|c8HwunQ2 zE_GkbW%;l~KDQfn(wUqf4!pwjytwYQAA##_f|DKVQh&Po#pKoar=N;u;H*I5+mZ`` z*WeN(jIM;dybgt;8}QQ|jcx=g{1|6C#n1*MLS&-Xl4&-&IO*MJRO-NG1$?WyeZ$Xmy!3R*$ zEl5(MTY(Cv8%rT?1u7ggHv$jfXF?jD%$Gxc1li#L4yfqgk)1{df&3iU1pE#1QuHJ; zQq+Z96g`D3(&<{QKd>%ME5&KeOb{Qb|h zI`^*&2UIvwJoERd>(@AB`hhO#s=&$a50tLwQT2zqX?z&?gMg3WAnu2{5jc+gq3-?v z67L_b9-O1%pNuZKwj}vEelX4lpgMj8&d0!q@Vjv?ugFwVj?A=3`9h{u%6?fkDI?;K zXJh=loSV$ZLVW=nJXuqS;d4MSqd{F1ja^>$_-Urp~3CR2ELtpjeif>Qh@; zF-x1P&%tN6HpRYMtFga8BOG(I(T=yZTOA*0l}@{wtAC)4c732NcO9p>`clV8el-^F}M@8J`1l7d&LKk4`LFQr|G=K<45L zbX7g;z=UOfO-){&nz8(XjDMN@fz)!h(^K2X{TRP^?85nT=N;J9Qe!>v^%}d=_lLlU z8}9x3vx|2e?74ElPMVYQZJwSJ(SkUVq>MltOZGygg4prx%v0@~3$utfvJuR zf9uzp*G9_g^*Rf={+v|p)+x!D-I@_-{ZjJ+AJpKrQF#Ls&M8sL)62%8#L0#{(R;Eu zd=7`hPFi45pe+MUcsveEA$fdud#=h0wEok9Phd`94(4Z|ZN6SetVHd=9B=C6)=Ia_ zr>Q*r+$7L8G1WIU*>br(lvD_ex(Wp{&!1P zmL3Rf3u@Mtfwm#3{TB{;<^iJ5QzN?eYco zVMjat_bkhW9nr8l_(bZST?2RrpwzKl0}goa`lFguK^6EpR}~G!XATX+e-*fm)k`@9 zx2Qq*uL8V^2GeYK&W5fEC(ASCGZ?nn;yVYn^J!S>yw2n5^$`dB_k5~#47C4m^ZTs~ delta 17333 zcmb_@33yf2wf{Q%-h1vmCpYs=Zo+WM00e>zf)W`7fg*#-q$Chj5V(?nq9HknM6?P> zdI6C^q0*|<2}An?Cn8vD)mCw+R;3{LtgU@&i@qur-*2sR7^3fe|Nr;Bmyi3~>$le0 zYp=b}-us-h@1^Ax)$$v)?)KtvxBefK^>>7di@aK2qODNqrt-J@+IL+ZSiX;FA}jV2 zU2khoM14nyeBiTS$WJF!d3w6m)AP<|sEY8}n!ZCT)9y|GO6#fpG<{er&UqM){5J+4 zMGmT)O!Uudh_v*Q`v-Phsvh*k8;I-?%518aa)n8g8c|tXpS82a3|3arTmqsH z?#iz28_v43#cI0;L_3_X>gv3aoska`WlIVTczX6I-Pha=jDyea>VNmy;Iq@E(M#ss z0DFW~({d?oO*cttZ)%j%(R8zv0ejO5Q*bmjn}XidWD3rv8%4-$S|;TRGh|Yk1AIgJ z?PyP<2BveycN3>EN780X(gxq=+O9tLob_Q#Gu+P&v6;n;| z?;2po+3eY3_HaR0pP!!fVN3IMUCsBOZR!I0XF6d-iSqi|>2i%I+BGrTvZ2`Wyv_<+tW0;N?n5MxO;2KB=ss6-KSZz8O3mz3 z+hHu%Rq`?zZLTCHs;9X>w~y)QX?Pl1zjP>Sj~P)%*dB36rv|M<^OAoG6Vo#oTjV)3 zg(>KX>e575k2q79Zl3b7#vlX^J5zfRbzTxv3S-EF0~eP$Iyn+D>@tVPw52eaJUxt| zlx`~s`7vwE5e@692huwV3JgpXPk9fC?XrUMxT@4H7@}@-_`~khFF+6b!j5uh*lrGV zDMm>~w2nR+tMQEnBWj~MlzgerTwlq4shNkXPtYgWfGWOIrmzZnhErx zzGp6}qQ-Q4JX$Jqho@NTVeT{cDfI|T94dwRj!EMz*Y)JR>EFe(RPwR(pW|glsnsPH zCHtP9BsRJMUCKn48agrb9+kvq?I~BWQeD+2oOFHiaqy-Mh#T`tuX(w6^i%rtaBp}5 zax#GBmka_@zXbJ_g)qB+#fHn^;>?=n1;?sM?TSk&GW}>#G6NUeQ<^KYuC$*a zQ)^%PFGao5g^9t27%}LIUPOj59`p;4MY&emJPf5L=G7ybxr#*Z_yE(ADESnErZ71@ zzL>vknajO7Y7g7%tI-u+gdR*(M@I5lNEK7Vj+P4%D(t9V!{+CpY#Gi9d;R^FovEOQ z^^D|x*vfl{_4<=%JgeDLuXngkSG@>|GLJiJn&0hOJHOwphiyF$Q&tOScjj{RE-oFB zg}x{(zy+nrT8xk_ukvQK4DD(jz5IH<3?8qZ!ti1!3tXuK5d2PeEIAjxse@q7)C=h? z#XXWQK(5eZ3!;w0{hS#|OxLot3v3H!zz)lTnb7Ad)JuO-e4&xejh5SOxs^(lHgmVF zPGt&FDU7nG+@)+u%yGT+OhT8bWuuW=#RRns6E3e}RM!M!XA-n!O*@;SEA?Ye?!&_w zu4Nb&Et-Cyq*rYYPqW{^;A!Sz&Pcw*PPxrIk{0nd^C+66ToK0+jx8YR?K0WG; zMO}lSq_8vWx_=;g9Cb(B$=|_RxFS22`hEJ_ZiU8cptAeQZW{H6{pIekUv|@|5AJ8| zVV`WR(SS1?nC5kby^&z*byTj%X}Al++&%0~-G(w6gee?M9fBzw?4G)vb)j%5YZ{NE z&Z;}O3`|Sm&Z8TwSj`IGv=r`2df1gUE$Rxp!p^nxFYNW*ci+YQu_*TAQ<>OS^CG%g zTD`-Tbx!ZXk+7LA7KdK$3fmFMUS{W6eA==PmS_aK`x{8FyjwV8M5AFxG$WEB8oreT z!cnj{VNbrrPVR^!97(UtJsoB5$zJ=?6k2i4}MmWpBsCQ%YM zc25fXBOJ~@!*ac}wwd%T)e?jEKQB`=#3@j0oe@(A1JKt!4n=Sb%958YC zA+X*_R3N!7g9*2o@p{(utPxnIl*Clg&Ag)ytZz;n+lJlDyjn8nN-At`!umt=SPZbM zWG1Jchsdgt4N;i5Je6sjNuDbr)F+6kRO5|cu~S-;ff95%7mZAkn524B3-c9>jsJlP zK1=?LE1T-iAttesbkx2Hehpu+O#(Gch1l?SKvOG+BX+mOEtElO6osZg}XuOpAafMZjrx+jJn^j z>Z!vKJ=VvMZ8mlx+Rx*?+Vj3@Y((wBK};3Zh>gK6PgWI(%B0LupM`Z2^@V+f=Vzx* zqDJ@F=BPjUEu?55D;eYUe~L-4tQNP1?^whwq5cX~%S(}lU3P3Z6!nTCsewl$VQ&TY zb8m`SI5It@Q0Ad7v((yp*@!y)lJ{h4N?$d+2{M^CyMm*-=dT|@@L^I3~(af-snc^lYa>AL-BhX11 zfd(&B-J`MpHj!}jzt18Z4QH4sSXXAq3fwu)xj1L9jcRN{uVZ~}8D$1mF_VSKyU}`s z4-GeAv*uaUyF|}Ee?Q>@szz3IuWOitw@X|^@t0Rx@M?p<#wQW>b*ghvcqbvPLIR3;SV zD@=DGRzG6U$5L6`RAHnrnax5x5>MtpKtHn2^dsJc$CT6Eh|7(m?1K-n097L!qH(o? z%Tt+Sx^yF+%+=^pPt;T9luIAwrI%|i+B|M7&&nwuBvrtgqOy{3Q7X=2BAgdVBnu&; zC_<6U;Wd*;|ERKPB=_9x*A?gVOXl$p0u>`ru^FiN`+S5z1F&1xv&OiLmq^((P>lHEAj601fovW=7^dq9WPAPMu8Y3Rv-Q%BFu z(DHsJ{pem{?~zd!OY%xg^#Yg*N3v98y@!loI5$-W9?h@+NVbV`kcwD1zpyb?!J-%9 zOcBeptY|?)1+wmL=9B7X%903oEQPIHPcQ7(&EOeS?p2LknwuN;yQsM)SkG(YTs`k7 zte5RmHp6H)zFEnnm+kyiCib^HmC+=dPlhZwwSpJVsmuX<`^%7dZ7z(9!`+g-kSYhP3&$yAmRDu zR~V=kJcsimmOM0-7H4HO+=%Ujo4|v^&6D_P0e_ilKO+f8$n)9uD>d>!;Ne27goZh(V%!zd-XDE-KQl41KH zm0)I;bEd{qIdGa7H$_w$i5+6H0Z3j_s6}^%t9QUo)hdI zX894ZdeMpp-ylQ{r%uVjLpcGOTa=7ND3b9`Bt}=;JPDmX4|0evq(W!(%x9CVRe|btq>F9f!uy=~QHB zu}&*g)ES_qfn+Q|KT0sxK-Te~b13IItMW08Llp9Td%lO(LI0o9yl#-YgbjNG?fE*rmCg7=~Il88e`Ykk`ePt57*Y-)6JiQ<_|ae(01a@McV*zi03Ob}DXu zub)S#qoArFKK}xhX8{MX`A)i!|646rR)v){iEm(GVqan2n-qgq?^pn@V^NKz8Ubl9@*9 z$d(9^9XSoA>C6!20i?Eu>S>p-?OT1F2GE5s@vOn1?X#$=*iPY z*wXZ@*tQ|eU@Y@a;wAk)*6$kHNCzlEzi?dy>~xI=Hn~|2xmo^~>mlGz+>ZdiaPJ1b z<@zPi?qQ$9;`3LR!Q?3+-P~N^VW}`rDvWb;h2OgSY6%+Pt^$7N@p%*UM_&-=^)H8J zrI+QszKAzW@uEE7G{K>QZxpiTHjz(?{0oua23FFpXrX~(Nq!0N?Oeuvf?pJ{+$wsN zTZRVy?BQl=z1-xL{{FD;%smg-nRg)?sEhXpp3G(JFY=Upmj9K{vBJcDpgZ!dk zH$Mm3;x&MI7zvH4MRT=y-h#&ZYy0%uz30&fj@8~6-AL;gHO3B+w~73T+6>LxqA|{+ zogkNKj4!GVV5g1cL3YN~cGmn;To77z?bu;YGLvyYDZQzsk zkAOoRC%if}gnYmcvut>}->8C_2H zSr!dq416}mW41WL&9EndkJ>AMxsHCoTO0#{+>AyAwxPgw(b#kz#edjF`*=PXrzcc` zR$yB3(EZ34!^)lv^CjM^G3mx(coTHHXBIRIf$UQYpMTLjBv*^%E-k|9q*bmht_gV6 zn`W`efw)7X8k1#zmOU6Re9MLXnm)`M;Iz>TMR@HX`Zay!pX{*Hx0W#xy$v_$1qqlC zbWZ*R<)C_t{U$f=aL~;bdn9+6&q23X?1O@v!S1lwjd26)Cl>pAP8--}lOdUJYz}%2 z4DFlQc&V+J8#I&gQ4uyHW}J+V`gUWs%5^9)LHTKr#TLZl4nGYyne@<4(}bBZgY+|t zaR)>6zQs6Zh%Vu+(Lm7h+zBd7lbM1IM$j=ujd&RYw%+GI#{E?RhBm|Pk@ax8G3=IA6h7^o%}`5`HJX$i;WdF9*>tq?Nlc0 zX^TBm$e!Ks$jLTiu^87|^dm9a%T41}u`RXO0b$22c8T*K!W31Qp1&%339PM;$=*xc zXD=apUz2rayzT2o3oSOI@O@toa#UJ2Bm0=I7gY(}NSpnC_LWkN#d zvoid~RPqfo>F&ab{;9OxWUd*Bss798;nOT{8rYK-+oaC|d&^=siS2!hEz9A0pIB^g zoa=pQv5#_@X%|R)uH*S&2Kg;o8RDShg|$PCvE%!^snDQuPNmfXpXS@e>{(m3>(MaChEUIpWo^hb+b z595_|%wmIJyNdp5v2n0nMJFt_SbtETP2XB}lApr41H)4Yq6PUW+YGOm@ttpIfXsq0xML%wn%*Zu2joe;XzDzg4b3 zWj+A9-!#fnyN=$lT$_To(RK8m#a0DFV8<=C1x+lZPc60!O)R7@EjBc-)4quQWwCL2 zt^P%%k2brTn?KpHm;zwv|0-869BeJ7EXz0)S0+V$O(w~tXoN6x;iYI|7uyxWR#GcH z;ZM<(r?7-*PE!(V#DcKeTTD5g^{=FT!i<%) z0JMz`SjNX3FZox|D;E1JY`4){7V{yQ+v%{yhH5YQ@1Tz@_8^kElRmfDS|qcYzP4DQ z{Z0QG3S4Yj=Q z^f!xMBkW6)p}{b01(Uglt6W*G62}(mBg|}m3!QJ;B(*IxLfA@jYPo?eH0~VRR+?hj z3bkk5d`P6xeJ(yKGA<7=*%9GJ_)nu%`D}O(XFoQ24rr&Z3JAZ{Y>e~!|5rKR_x~7= zk%|_>+BMEr)KxSZdK;aPIK3e|XcXe>^tU|1tNcU*Di{3{q-$kP$khB0PbYd^lpEMB z4S$LxH2SA#&XtR$`HQ8KY}N375eg~1UxY$7yjd8bkR7iXp^yVF7@-iqT7*JQyk>+# zF1(QZu)_b_|A(m`cXFaP26-g1aa?Dozd_@m-v_b&HFA`2M!RHS9eC_kv;hsH)@Ee7ZGz@}sv zS@Xle|3B6kR;K3;8Xxf}Dv~SuzmYHGlTrE8h~;zfwBGoi4FA9SbAy`l>Cop!sKvF{ z$i#EIy2FrpE{9q&5{mj^E!gN*jDbc<@L)8EXP^*1Zu$)0{)B-#vRsM}a*=Urq_xgcGv)`sA?5Cx=Z- z;GRXFMO7^NH>XXQ=s#SSIUqXJm_#|*opulF= zxS1ttSA2qPiF&ViISz4HlOdQd*bVrNtsFSm-Y(G3R`-W1%6 z7FR&?nUQ^q{Y?-ZJuAxK{%tR0{Bgz_d!>5Wf1mv!b)smey-M2nO_@l z`Xb@bS0LXBs!n?IkaD_KA<(YK-(WqN(5X(dyJ9+ri|Y%euXP#^=2F3hf}CO>y;7}- zU!>P*ZSgUBoAh;w`Z72}U!q=4EZ0|KgxdA>GD7RIuuB|9o#eG!b@+d(cWQMx59%+e z8#A5(#&ZwoUu!Q0f2+Sl=a;+&oLTY?@M6eY5n9LhgGaN!(cS7)5jnSNPM00X<9Ck= z=6Rei(F4H&&WFYGUG4mmY0h`GO_4d!@Wrar-Yeek++&FOX}M07s<`m5^J%$qdt?~* zi0eV^_DG52pf(0GIiadDzHlByExR+Jxo;}%Bsg1)!SG88fC+4C>V?p-`;4StO82lplw;22?w_P2Ik8#&( z^Aej%(aYlp|JP^I@o$x-&4;6I7*|fIXm64USHB zRI1YGt!}sN<>EWtyn;5M*2%~gcf$5r_D|73RpJ-GWa0_;d~JNelW1#K^>+lJpD^B9yX-bR*aDp2BCVuc&z?B%Hv z&!OTuQaneBC+~^3*q5NmDW0#9`gG5DNqwr~1vypm;(VCUVFONm0ws=i)`imz(uZrvI z;`El_yW;u*TwS;?vgb$ebdgVc73kMK1BSFiz=VdMjZ=5+1h7K;+T)=C+9}9`H7CxG zucHIO?$k&rUr(Az)uG<_KDIs3+ral!n*+V^bGN;~5_%qpv$$Yy>Q6!7`IHMBK|O)v zL_Y;sE#XGcT$&jiB|&Sb0dlSA=Zd~XG_9gpDVjETmV~y8euwDqCxb)m7geXI4&W8E zBy>Q69u>_o(Hs+vrg1-P8vEEZ?oC28C88-2O|@u7h-QRnYDF_wG;>AMDw>trN&|sb zimDBW>@@Dj4l&=a&E@1o(;Ww|!ga55eI+o*&hs zsS&wWue|Dtf}P94-nDZ&JP@krN^h7rENck!$>O4LH?`p+yX>B5xOY zhsgUy?iBfm$VWw{fHZ(N{o}zRkrM%KY`DlZf-QpE1@{Yfiq8>|DJb(_3rdb4xUM1( z7r8pP1wX;45xG|MEh4vy+y=yQ70rIZBZ3r?v_d?cwGdBddx$-YM4y0+6h*FP+3?i} zX%TD{bDLDyFPct-cqMp5k=h#+N31;Lt3-W#?H?$6}pjtEkWd;<~_9pB^x?(CCJ*{`%v6a}e%Q{1w7|t|FYI zTps8ST$RuAd$}w>ms<+_s-O?BFRPERPoXg!uQ4v;U}k_WxTR z`%jEt0Q_a%2;k7b7z0Iid^~V+;!-wbvfNfM75Io>R?ik=w!HI&m>5HM64aDK_9wE2qu+!)`H*+GFu{y*~ zU*)sB%UKV6Q}C7qdmhVd0)8IkW@hWwnT1&H7TAqJMRqO zKZjk%1?9QMr$3(cdt(|6#KFjPyyz>MfeWH&CJh75!tc5izOSeTUWJDRMYHj{6-704 zG4L81i=%{VfeNqx;~*~pD%{&AKwbz`vJJ9_#w<9$ZL_e!tZL9 zK=T+-(c^fk!5cPE(Gy5m!;c`AL4FbmD@^^Hfcuc7qNk7^KMoocq^Vjj;N@Bw4qT=| z4$%zAgVE_!derPm#&*YJj;9#z-zM{!?qf0j?IC`A7qdxOmSvan)+~pVH(G)37JnBtWI>HtzFOjWXmS?U_t5iF z-$Ns%zK2Nad+1~QkPUV()nwsd4339l=`poc{jbrM>htkgqh&hRXmRJO6mreiVy@S< zxvuv#r~4#q?`bj5d)gS!yR=jvr_$-iYKPnVSI+L6zVzy>bk&?5=@oNI!gFWO9y!0^ z`h~SOjaXRQ&`>!ry?M?==iu41s|JZ@hN>FW@y(o*dRi@5-|@_X3sgsJ(Y>~gr>-BO zcYJ$;PfPoj59*k?-0kjYXq{p2c;!}?I}~aA`i=iMG;7qR1AX_*UGVtQ^zpUR^D?z4 zx$!?fBdshsi(PKPwzL7FS%N;j{+3%@#45M62ihHR}i-DAU)^lvv5Y4g&5-B2BN6luua6Vg-x ze!&oF9h)Awak6ciGc&z<MG7@hF}-|Ksr|>{)-~y$Z%S$# z(!bxdK--oca!(&^M|#0Mqw}`~i)>mT(z4BJV4I@|**#=0(v+>^jeB-!9k*Y(IcQG5{__U*uj@Onu3zQVmHp-poI9_!ZcbI-xY%BltMTcFt#2Sc^ROMVe--R?s520sewMX1-RJ&y)hk0f!aG0H+WR{H3k%@N Ap#T5? From ddc264a771b27807a35d12fd3645d4693b4a21f8 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Mon, 8 Jun 2026 12:49:44 +0800 Subject: [PATCH 163/202] Update Invoke-CippTestGenericTest002.ps1 --- .../GenericTests/Identity/Invoke-CippTestGenericTest002.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest002.ps1 b/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest002.ps1 index 77314747c732..90b33089f235 100644 --- a/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest002.ps1 +++ b/Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest002.ps1 @@ -54,11 +54,11 @@ function Invoke-CippTestGenericTest002 { $LicList = ($Entry.Value.Licenses | Sort-Object) -join ', ' $null = $Result.Append("| $DisplayName | $LicList |`n") $DisplayCount++ - if ($DisplayCount -ge 100) { break } + if ($DisplayCount -ge 500) { break } } - if ($UserLicenseMap.Count -gt 100) { - $null = $Result.Append("`n*Showing 100 of $($UserLicenseMap.Count) licensed users.*`n") + if ($UserLicenseMap.Count -gt 500) { + $null = $Result.Append("`n*Showing 500 of $($UserLicenseMap.Count) licensed users.*`n") } Add-CippTestResult -TenantFilter $Tenant -TestId 'GenericTest002' -TestType 'Identity' -Status 'Informational' -ResultMarkdown $Result -Risk 'Informational' -Name 'User License Overview' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Tenant Overview' From 2a63cfc7eb0e457762a8fa3c50b9121821becb12 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Mon, 8 Jun 2026 13:27:58 +0800 Subject: [PATCH 164/202] Update Get-CIPPAlertQuotaUsed.ps1 --- .../Public/Alerts/Get-CIPPAlertQuotaUsed.ps1 | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertQuotaUsed.ps1 b/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertQuotaUsed.ps1 index 45ab07b93cd6..a20a9c38d406 100644 --- a/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertQuotaUsed.ps1 +++ b/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertQuotaUsed.ps1 @@ -8,34 +8,32 @@ function Get-CIPPAlertQuotaUsed { [Parameter(Mandatory = $false)] [Alias('input')] $InputValue, + [Parameter(Mandatory)] $TenantFilter ) + $Threshold = if ($InputValue.QuotaUsedQuota) { [int]$InputValue.QuotaUsedQuota } else { 90 } + $ExcludedRaw = Get-CIPPTextReplacement -TenantFilter $TenantFilter -Text ([string]$InputValue.QuotaUsedExcludedMailboxes) + $Excluded = @($ExcludedRaw -split ',' | ForEach-Object { $_.Trim().ToLower() } | Where-Object { $_ }) + try { - $AlertData = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/reports/getMailboxUsageDetail(period='D7')?`$format=application/json" -tenantid $TenantFilter + $AlertData = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/reports/getMailboxUsageDetail(period='D7')?`$format=application/json&`$top=999" -tenantid $TenantFilter } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Alerts' -tenant $TenantFilter -message "Mailbox quota Alert: Unable to get mailbox usage: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage return } + $OverQuota = $AlertData | ForEach-Object { - if ([string]::IsNullOrEmpty($_.StorageUsedInBytes) -or [string]::IsNullOrEmpty($_.prohibitSendReceiveQuotaInBytes) -or $_.StorageUsedInBytes -eq 0 -or $_.prohibitSendReceiveQuotaInBytes -eq 0) { return } - try { - $PercentLeft = [math]::round(($_.storageUsedInBytes / $_.prohibitSendReceiveQuotaInBytes) * 100) - } catch { $PercentLeft = 100 } - try { - if ([int]$InputValue -gt 0) { - $Value = [int]$InputValue - } else { - $Value = 90 - } - } catch { - $Value = 90 - } - if ($PercentLeft -gt $Value) { + if (!$_.StorageUsedInBytes -or !$_.prohibitSendReceiveQuotaInBytes) { return } + if ($Excluded -contains $_.userPrincipalName.ToLower()) { return } + $UsagePercent = [math]::Round(($_.storageUsedInBytes / $_.prohibitSendReceiveQuotaInBytes) * 100) + if ($UsagePercent -gt $Threshold) { [PSCustomObject]@{ - Message = "$($_.userPrincipalName): Mailbox is more than $($value)% full. Mailbox is $PercentLeft% full" + Message = "$($_.userPrincipalName): Mailbox is more than $($Threshold)% full. Mailbox is $UsagePercent% full" Owner = $_.userPrincipalName RecipientType = $_.recipientType - UsagePercent = $PercentLeft + UsagePercent = $UsagePercent StorageUsedInBytes = $_.storageUsedInBytes ProhibitSendReceiveQuotaInBytes = $_.prohibitSendReceiveQuotaInBytes Tenant = $TenantFilter From 64f30df6a54328ba3e886cd83d5b7417fb153ff2 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Mon, 8 Jun 2026 14:51:46 +0800 Subject: [PATCH 165/202] Update Start-DurableCleanup.ps1 --- .../Public/Entrypoints/Timer Functions/Start-DurableCleanup.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-DurableCleanup.ps1 b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-DurableCleanup.ps1 index edc12c427324..aec18b23828f 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-DurableCleanup.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-DurableCleanup.ps1 @@ -18,7 +18,7 @@ function Start-DurableCleanup { [int]$MaxDuration = 86400 ) - if $env:CIPPNG -eq 'true' { + if ($env:CIPPNG -eq 'true') { return } From 34112ffd912693d51f62e1f98e12a5009fc556fd Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 8 Jun 2026 11:30:26 +0200 Subject: [PATCH 166/202] temporary 1 day to clear old ips --- Modules/CIPPCore/Public/Get-CIPPGeoIPLocation.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Get-CIPPGeoIPLocation.ps1 b/Modules/CIPPCore/Public/Get-CIPPGeoIPLocation.ps1 index b6eaabf73f71..7debffaef33e 100644 --- a/Modules/CIPPCore/Public/Get-CIPPGeoIPLocation.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPGeoIPLocation.ps1 @@ -5,8 +5,8 @@ function Get-CIPPGeoIPLocation { ) $CacheGeoIPTable = Get-CippTable -tablename 'cachegeoip' - $30DaysAgo = (Get-Date).AddDays(-30).ToString('yyyy-MM-ddTHH:mm:ssZ') - $Filter = "PartitionKey eq 'IP' and RowKey eq '$IP' and Timestamp ge datetime'$30DaysAgo'" + $1DayAgo = (Get-Date).AddDays(-1).ToString('yyyy-MM-ddTHH:mm:ssZ') + $Filter = "PartitionKey eq 'IP' and RowKey eq '$IP' and Timestamp ge datetime'$1DayAgo'" $GeoIP = Get-CippAzDataTableEntity @CacheGeoIPTable -Filter $Filter if ($GeoIP -and $GeoIP.Data) { return ($GeoIP.Data | ConvertFrom-Json) From bfb0c17a9ccd997e71218c4e5e71c68868fbd941 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 8 Jun 2026 11:31:26 +0200 Subject: [PATCH 167/202] tools update --- Tools/Configuration/1.6.0/Configuration.psd1 | 77 + Tools/Configuration/1.6.0/Configuration.psm1 | 787 ++++++++++ Tools/Configuration/1.6.0/PSGetModuleInfo.xml | 162 ++ Tools/Metadata/1.5.7/Metadata.psd1 | 50 + Tools/Metadata/1.5.7/Metadata.psm1 | 1210 ++++++++++++++ Tools/Metadata/1.5.7/PSGetModuleInfo.xml | 154 ++ Tools/ModuleBuilder/3.1.8/ModuleBuilder.psd1 | 54 + Tools/ModuleBuilder/3.1.8/ModuleBuilder.psm1 | 1390 +++++++++++++++++ Tools/ModuleBuilder/3.1.8/PSGetModuleInfo.xml | 165 ++ .../3.1.8/en-US/about_ModuleBuilder.help.txt | 88 ++ 10 files changed, 4137 insertions(+) create mode 100644 Tools/Configuration/1.6.0/Configuration.psd1 create mode 100644 Tools/Configuration/1.6.0/Configuration.psm1 create mode 100644 Tools/Configuration/1.6.0/PSGetModuleInfo.xml create mode 100644 Tools/Metadata/1.5.7/Metadata.psd1 create mode 100644 Tools/Metadata/1.5.7/Metadata.psm1 create mode 100644 Tools/Metadata/1.5.7/PSGetModuleInfo.xml create mode 100644 Tools/ModuleBuilder/3.1.8/ModuleBuilder.psd1 create mode 100644 Tools/ModuleBuilder/3.1.8/ModuleBuilder.psm1 create mode 100644 Tools/ModuleBuilder/3.1.8/PSGetModuleInfo.xml create mode 100644 Tools/ModuleBuilder/3.1.8/en-US/about_ModuleBuilder.help.txt diff --git a/Tools/Configuration/1.6.0/Configuration.psd1 b/Tools/Configuration/1.6.0/Configuration.psd1 new file mode 100644 index 000000000000..597a38ca6f78 --- /dev/null +++ b/Tools/Configuration/1.6.0/Configuration.psd1 @@ -0,0 +1,77 @@ +@{ + +# Script module or binary module file associated with this manifest. +ModuleToProcess = 'Configuration.psm1' + +# Version number of this module. +ModuleVersion = '1.6.0' + +# ID used to uniquely identify this module +GUID = 'e56e5bec-4d97-4dfd-b138-abbaa14464a6' + +# Author of this module +Author = @('Joel Bennett') + +# Company or vendor of this module +CompanyName = 'HuddledMasses.org' + +# Copyright statement for this module +Copyright = 'Copyright (c) 2014-2021 by Joel Bennett, all rights reserved.' + +# Description of the functionality provided by this module +Description = 'A module for storing and reading configuration values, with full PS Data serialization, automatic configuration for modules and scripts, etc.' + +# Exports - populated by the build +FunctionsToExport = @('Export-Configuration','Get-ConfigurationPath','Get-ParameterValue','Import-Configuration','Import-ParameterConfiguration') +CmdletsToExport = @() +VariablesToExport = @() +AliasesToExport = 'Get-StoragePath' +RequiredModules = @('Metadata') + +# List of all files packaged with this module +FileList = @('.\Configuration.psd1','.\Configuration.psm1') + +PrivateData = @{ + # Allows overriding the default paths where Configuration stores it's configuration + # Within those folders, the module assumes a "powershell" folder and creates per-module configuration folders + PathOverride = @{ + # Where the user's personal configuration settings go. + # Highest presedence, overrides all other settings. + # Defaults to $Env:LocalAppData on Windows + # Defaults to $Env:XDG_CONFIG_HOME elsewhere ($HOME/.config/) + UserData = "" + # On some systems there are "roaming" user configuration stored in the user's profile. Overrides machine configuration + # Defaults to $Env:AppData on Windows + # Defaults to $Env:XDG_CONFIG_DIRS elsewhere (or $HOME/.local/share/) + EnterpriseData = "" + # Machine specific configuration. Overrides defaults, but is overriden by both user roaming and user local settings + # Defaults to $Env:ProgramData on Windows + # Defaults to /etc/xdg elsewhere + MachineData = "" + } + # PSData is module packaging and gallery metadata embedded in PrivateData + # It's for the PoshCode and PowerShellGet modules + # We had to do this because it's the only place we're allowed to extend the manifest + # https://connect.microsoft.com/PowerShell/feedback/details/421837 + PSData = @{ + # The semver pre-release version information + PreRelease = '' + + # Keyword tags to help users find this module via navigations and search. + Tags = @('Development','Configuration','Settings','Storage') + + # The web address of this module's project or support homepage. + ProjectUri = "https://github.com/PoshCode/Configuration" + + # The web address of this module's license. Points to a page that's embeddable and linkable. + LicenseUri = "http://opensource.org/licenses/MIT" + + # Release notes for this particular version of the module + ReleaseNotes = ' + - Extract the Metadata module + - Add support for arbitrary AllowedVariables + ' + } +} + +} diff --git a/Tools/Configuration/1.6.0/Configuration.psm1 b/Tools/Configuration/1.6.0/Configuration.psm1 new file mode 100644 index 000000000000..134613ca6bfa --- /dev/null +++ b/Tools/Configuration/1.6.0/Configuration.psm1 @@ -0,0 +1,787 @@ +#Region '.\Header\param.ps1' 0 +# Allows you to override the Scope storage paths (e.g. for testing) +param( + $Converters = @{}, + $EnterpriseData, + $UserData, + $MachineData +) + +if ($Converters.Count) { + Add-MetadataConverter $Converters +} +#EndRegion '.\Header\param.ps1' 12 +#Region '.\Private\InitializeStoragePaths.ps1' 0 +function InitializeStoragePaths { + [CmdletBinding()] + param( + $EnterpriseData, + $UserData, + $MachineData + ) + + $PathOverrides = $MyInvocation.MyCommand.Module.PrivateData.PathOverride + + # Where the user's personal configuration settings go. + # Highest presedence, overrides all other settings. + if ([string]::IsNullOrWhiteSpace($UserData)) { + if (!($UserData = $PathOverrides.UserData)) { + if ($IsLinux -or $IsMacOs) { + # Defaults to $Env:XDG_CONFIG_HOME on Linux or MacOS ($HOME/.config/) + if (!($UserData = $Env:XDG_CONFIG_HOME)) { + $UserData = Join-Path $HOME .config/ + } + } else { + # Defaults to $Env:LocalAppData on Windows + if (!($UserData = $Env:LocalAppData)) { + $UserData = [Environment]::GetFolderPath("LocalApplicationData") + } + } + } + } + + # On some systems there are "roaming" user configuration stored in the user's profile. Overrides machine configuration + if ([string]::IsNullOrWhiteSpace($EnterpriseData)) { + if (!($EnterpriseData = $PathOverrides.EnterpriseData)) { + if ($IsLinux -or $IsMacOs) { + # Defaults to the first value in $Env:XDG_CONFIG_DIRS on Linux or MacOS (or $HOME/.local/share/) + if (!($EnterpriseData = @($Env:XDG_CONFIG_DIRS -split ([IO.Path]::PathSeparator))[0] )) { + $EnterpriseData = Join-Path $HOME .local/share/ + } + } else { + # Defaults to $Env:AppData on Windows + if (!($EnterpriseData = $Env:AppData)) { + $EnterpriseData = [Environment]::GetFolderPath("ApplicationData") + } + } + } + } + + # Machine specific configuration. Overrides defaults, but is overriden by both user roaming and user local settings + if ([string]::IsNullOrWhiteSpace($MachineData)) { + if (!($MachineData = $PathOverrides.MachineData)) { + if ($IsLinux -or $IsMacOs) { + # Defaults to /etc/xdg elsewhere + $XdgConfigDirs = $Env:XDG_CONFIG_DIRS -split ([IO.Path]::PathSeparator) | Where-Object { $_ -and (Test-Path $_) } + if (!($MachineData = if ($XdgConfigDirs.Count -gt 1) { + $XdgConfigDirs[1] + })) { + $MachineData = "/etc/xdg/" + } + } else { + # Defaults to $Env:ProgramData on Windows + if (!($MachineData = $Env:ProgramAppData)) { + $MachineData = [Environment]::GetFolderPath("CommonApplicationData") + } + } + } + } + + Join-Path $EnterpriseData powershell + Join-Path $UserData powershell + Join-Path $MachineData powershell +} + +$EnterpriseData, $UserData, $MachineData = InitializeStoragePaths -EnterpriseData $EnterpriseData -UserData $UserData -MachineData $MachineData +#EndRegion '.\Private\InitializeStoragePaths.ps1' 72 +#Region '.\Private\ParameterBinder.ps1' 0 +function ParameterBinder { + if (!$Module) { + [System.Management.Automation.PSModuleInfo]$Module = . { + $Command = ($CallStack)[0].InvocationInfo.MyCommand + $mi = if ($Command.ScriptBlock -and $Command.ScriptBlock.Module) { + $Command.ScriptBlock.Module + } else { + $Command.Module + } + + if ($mi -and $mi.ExportedCommands.Count -eq 0) { + if ($mi2 = Get-Module $mi.ModuleBase -ListAvailable | Where-Object { ($_.Name -eq $mi.Name) -and $_.ExportedCommands } | Select-Object -First 1) { + $mi = $mi2 + } + } + $mi + } + } + + if (!$CompanyName) { + [String]$CompanyName = . { + if ($Module) { + $CName = $Module.CompanyName -replace "[$([Regex]::Escape(-join[IO.Path]::GetInvalidFileNameChars()))]", "_" + if ($CName -eq "Unknown" -or -not $CName) { + $CName = $Module.Author + if ($CName -eq "Unknown" -or -not $CName) { + $CName = "AnonymousModules" + } + } + $CName + } else { + "AnonymousScripts" + } + } + } + + if (!$Name) { + [String]$Name = $(if ($Module) { + $Module.Name + } <# else { ($CallStack)[0].InvocationInfo.MyCommand.Name } #>) + } + + if (!$DefaultPath -and $Module) { + [String]$DefaultPath = $(if ($Module) { + Join-Path $Module.ModuleBase Configuration.psd1 + }) + } +} +#EndRegion '.\Private\ParameterBinder.ps1' 49 +#Region '.\Public\Export-Configuration.ps1' 0 +function Export-Configuration { + <# + .Synopsis + Exports a configuration object to a specified path. + .Description + Exports the configuration object to a file, by default, in the Roaming AppData location + + NOTE: this exports the FULL configuration to this file, which will override both defaults and local machine configuration when Import-Configuration is used. + .Example + @{UserName = $Env:UserName; LastUpdate = [DateTimeOffset]::Now } | Export-Configuration + + This example shows how to use Export-Configuration in your module to cache some data. + + .Example + Get-Module Configuration | Export-Configuration @{UserName = $Env:UserName; LastUpdate = [DateTimeOffset]::Now } + + This example shows how to use Export-Configuration to export data for use in a specific module. + #> + # PSSCriptAnalyzer team refuses to listen to reason. See bugs: #194 #283 #521 #608 + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "")] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Callstack', Justification = 'This is referenced in ParameterBinder')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Module', Justification = 'This is referenced in ParameterBinder')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'DefaultPath', Justification = 'This is referenced in ParameterBinder')] + [CmdletBinding(DefaultParameterSetName = '__ModuleInfo', SupportsShouldProcess)] + param( + # Specifies the objects to export as metadata structures. + # Enter a variable that contains the objects or type a command or expression that gets the objects. + # You can also pipe objects to Export-Metadata. + [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)] + $InputObject, + + # Serialize objects as hashtables + [switch]$AsHashtable, + + # A callstack. You should not ever pass this. + # It is used to calculate the defaults for all the other parameters. + [Parameter(ParameterSetName = "__CallStack")] + [System.Management.Automation.CallStackFrame[]]$CallStack = $(Get-PSCallStack), + + # The Module you're importing configuration for + [Parameter(ParameterSetName = "__ModuleInfo", ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] + [System.Management.Automation.PSModuleInfo]$Module, + + + # An optional module qualifier (by default, this is blank) + [Parameter(ParameterSetName = "ManualOverride", Mandatory = $true, ValueFromPipelineByPropertyName = $true)] + [Alias("Author")] + [String]$CompanyName, + + # The name of the module or script + # Will be used in the returned storage path + [Parameter(ParameterSetName = "ManualOverride", Mandatory = $true, ValueFromPipelineByPropertyName = $true)] + [String]$Name, + + # DefaultPath is IGNORED. + # The parameter was here to match Import-Configuration, but it is meaningless in Export-Configuration + # The only reason I haven't removed it is that I don't want to break any code that might be using it. + # TODO: If we release a breaking changes Configuration 2.0, remove this parameter + [Parameter(ParameterSetName = "ManualOverride", ValueFromPipelineByPropertyName = $true)] + [Alias("ModuleBase")] + [String]$DefaultPath, + + # The scope to save at, defaults to Enterprise (which returns a path in "RoamingData") + [Parameter(ParameterSetName = "ManualOverride")] + [ValidateSet("User", "Machine", "Enterprise")] + [string]$Scope = "Enterprise", + + # The version for saved settings -- if set, will be used in the returned path + # NOTE: this is *NOT* calculated from the CallStack + [Version]$Version + ) + process { + . ParameterBinder + if (!$Name) { + throw "Could not determine the storage name, Export-Configuration should only be called from inside a script or module, or by piping ModuleInfo to it." + } + + $Parameters = @{ + CompanyName = $CompanyName + Name = $Name + } + if ($Version) { + $Parameters.Version = $Version + } + + $MachinePath = Get-ConfigurationPath @Parameters -Scope $Scope + + $ConfigurationPath = Join-Path $MachinePath "Configuration.psd1" + + $InputObject | Export-Metadata $ConfigurationPath -AsHashtable:$AsHashtable + } +} +#EndRegion '.\Public\Export-Configuration.ps1' 93 +#Region '.\Public\Get-ConfigurationPath.ps1' 0 +function Get-ConfigurationPath { + #.Synopsis + # Gets an storage path for configuration files and data + #.Description + # Gets an AppData (or roaming profile) or ProgramData path for configuration and data storage. The folder returned is guaranteed to exist (which means calling this function actually creates folders). + # + # Get-ConfigurationPath is designed to be called from inside a module function WITHOUT any parameters. + # + # If you need to call Get-ConfigurationPath from outside a module, you should pipe the ModuleInfo to it, like: + # Get-Module Powerline | Get-ConfigurationPath + # + # As a general rule, there are three scopes which result in three different root folders + # User: $Env:LocalAppData + # Machine: $Env:ProgramData + # Enterprise: $Env:AppData (which is the "roaming" folder of AppData) + # + #.NOTES + # 1. This command is primarily meant to be used in modules, to find a place where they can serialize data for storage. + # 2. It's techincally possible for more than one module to exist with the same name. + # The command uses the Author or Company as a distinguishing name. + # + #.Example + # $CacheFile = Join-Path (Get-ConfigurationPath) Data.clixml + # $Data | Export-CliXML -Path $CacheFile + # + # This example shows how to use Get-ConfigurationPath with Export-CliXML to cache data as clixml from inside a module. + [Alias("Get-StoragePath")] + # [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Callstack', Justification = 'This is referenced in ParameterBinder')] + # [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Module', Justification = 'This is referenced in ParameterBinder')] + [CmdletBinding(DefaultParameterSetName = '__ModuleInfo')] + param( + # The scope to save at, defaults to Enterprise (which returns a path in "RoamingData") + [ValidateSet("User", "Machine", "Enterprise")] + [string]$Scope = "Enterprise", + + # A callstack. You should not ever pass this. + # It is used to calculate the defaults for all the other parameters. + [Parameter(ParameterSetName = "__CallStack")] + [System.Management.Automation.CallStackFrame[]]$CallStack = $(Get-PSCallStack), + + # The Module you're importing configuration for + [Parameter(ParameterSetName = "__ModuleInfo", ValueFromPipeline = $true)] + [System.Management.Automation.PSModuleInfo]$Module, + + # An optional module qualifier (by default, this is blank) + [Parameter(ParameterSetName = "ManualOverride", Mandatory = $true, ValueFromPipelineByPropertyName = $true)] + [Alias("Author")] + [String]$CompanyName, + + # The name of the module or script + # Will be used in the returned storage path + [Parameter(ParameterSetName = "ManualOverride", Mandatory = $true, ValueFromPipelineByPropertyName = $true)] + [String]$Name, + + # The version for saved settings -- if set, will be used in the returned path + # NOTE: this is *NOT* calculated from the CallStack + [Version]$Version, + + # By default, Get-ConfigurationPath creates the folder if it doesn't already exist + # This switch allows overriding that behavior: if set, does not create missing paths + [Switch]$SkipCreatingFolder + ) + begin { + $PathRoot = $(switch ($Scope) { + "Enterprise" { + $EnterpriseData + } + "User" { + $UserData + } + "Machine" { + $MachineData + } + # This should be "Process" scope, but what does that mean? + # "AppDomain" { $MachineData } + default { + $EnterpriseData + } + }) + if (Test-Path $PathRoot) { + $PathRoot = Resolve-Path $PathRoot + } elseif (!$SkipCreatingFolder) { + Write-Warning "The $Scope path $PathRoot cannot be found" + } + } + + process { + . ParameterBinder + + if (!$Name) { + Write-Error "Empty Name ($Name) in $($PSCmdlet.ParameterSetName): $($PSBoundParameters | Format-List | Out-String)" + throw "Could not determine the storage name, Get-ConfigurationPath should only be called from inside a script or module." + } + $CompanyName = $CompanyName -replace "[$([Regex]::Escape(-join[IO.Path]::GetInvalidFileNameChars()))]", "_" + if ($CompanyName -and $CompanyName -ne "Unknown") { + $PathRoot = Join-Path $PathRoot $CompanyName + } + + $PathRoot = Join-Path $PathRoot $Name + + if ($Version) { + $PathRoot = Join-Path $PathRoot $Version + } + + if (Test-Path $PathRoot -PathType Leaf) { + throw "Cannot create folder for Configuration because there's a file in the way at $PathRoot" + } + + if (!$SkipCreatingFolder -and !(Test-Path $PathRoot -PathType Container)) { + $null = New-Item $PathRoot -Type Directory -Force + } + + # Note: this used to call Resolve-Path + $PathRoot + } +} +#EndRegion '.\Public\Get-ConfigurationPath.ps1' 117 +#Region '.\Public\Get-ParameterValue.ps1' 0 +function Get-ParameterValue { + <# + .SYNOPSIS + Get parameter values from PSBoundParameters + DefaultValues and optionally, a configuration file + .DESCRIPTION + This function gives command authors an easy way to combine default parameter values and actual arguments. + It also supports user-specified default parameter values loaded from a configuration file. + + It returns a hashtable (like PSBoundParameters) which combines these parameter defaults with parameter values passed by the caller. + #> + [CmdletBinding()] + param( + # The base name of a configuration file to read defaults from + # If specified, the command will read a ".psd1" file with this name + # Suggested Value: $MyInvocation.MyCommand.Noun + [string]$FromFile, + + # If your configuration file has defaults for multiple commands, pass + # the top-level key which contains defaults for this invocation + [string]$CommandKey, + + # Allows extending the valid variables which are allowed to be referenced in configuration + # BEWARE: This exposes the value of these variables in the calling context to the configuration file + # You are reponsible to only allow variables which you know are safe to share + [String[]]$AllowedVariables + ) + + $CallersInvocation = $PSCmdlet.SessionState.PSVariable.GetValue("MyInvocation") + $BoundParameters = @{} + $CallersInvocation.BoundParameters + $AllParameters = $CallersInvocation.MyCommand.Parameters + + if ($FromFile) { + $FromFile = [IO.Path]::ChangeExtension($FromFile, ".psd1") + } + + $FileDefaults = if ($FromFile -and (Test-Path $FromFile)) { + $MetadataOptions = @{ + AllowedVariables = $AllowedVariables + PSVariable = $PSCmdlet.SessionState.PSVariable + ErrorAction = "SilentlyContinue" + } + Write-Debug "Importing $FromFile" + $FileValues = Import-Metadata $FromFile @MetadataOptions + if ($CommandKey) { + $FileValues = $FileValues.$CommandKey + } + $FileValues + } else { + @{} + } + + # Don't support getting common parameters from the config file + $CommonParameters = [System.Management.Automation.Cmdlet]::CommonParameters + + [System.Management.Automation.Cmdlet]::OptionalCommonParameters + + # Layer the defaults below config below actual parameter values + foreach ($parameter in $AllParameters.GetEnumerator().Where({ $_.Key -notin $CommonParameters })) { + Write-Debug " Parameter: $($parameter.key)" + $key = $parameter.Key + + # Support parameter aliases in the config file by changing the alias to the parameter name + # If the value is not in the file defaults AND was not set by the user ... + if ($FromFile -and -not $FileDefaults.ContainsKey($key) -and -not $BoundParameters.ContainsKey($key)) { + # Check if any of the aliases are in the file defaults + Write-Debug " Aliases: $($parameter.Value.Aliases -join ', ')" + foreach ($k in @($parameter.Value.Aliases)) { + if ($null -ne $k -and $FileDefaults.ContainsKey($k)) { + Write-Debug " ... Update FileDefaults[$key] from $k" + $FileDefaults[$key] = $FileDefaults[$k] + $null = $FileDefaults.Remove($k) + break + } + } + } + + # Bound parameter values > build.psd1 values > default parameters values + if ($CallersInvocation) { + # If it's in the file defaults (now) AND it was not already set at a higher precedence + if ($FromFile -and $FileDefaults.ContainsKey($Parameter) -and -not ($BoundParameters.ContainsKey($Parameter))) { + Write-Debug "Export $Parameter = $($FileDefaults[$Parameter])" + $BoundParameters[$Parameter] = $FileDefaults[$Parameter] + # Set the variable in the _callers_ SessionState as well as our return hashtable + $PSCmdlet.SessionState.PSVariable.Set($Parameter, $FileDefaults[$Parameter]) + # If it's still NOT in the file defaults and was not already set, check if there's a default value + } elseif (-not $FileDefaults.ContainsKey($key) -and -not $BoundParameters.ContainsKey($key)) { + # Reading the current value of the $key variable returns either the bound parameter or the default + if ($null -ne ($value = $PSCmdlet.SessionState.PSVariable.Get($key).Value)) { + Write-Debug " From Default: $($BoundParameters[$key] -join ', ')" + if ($value -ne ($null -as $parameter.Value.ParameterType)) { + $BoundParameters[$key] = $value + } + } + # Otherwise, it was set by the user, or ... + } elseif ($BoundParameters[$key]) { + Write-Debug " From Parameter: $($BoundParameters[$key] -join ', ')" + # We'll set it from the file + } elseif ($FileDefaults[$key]) { + Write-Debug " From File: $($FileDefaults[$key] -join ', ')" + $BoundParameters[$key] = $FileDefaults[$key] + } + } + } + + $BoundParameters +} +#EndRegion '.\Public\Get-ParameterValue.ps1' 106 +#Region '.\Public\Import-Configuration.ps1' 0 +function Import-Configuration { + #.Synopsis + # Import the full, layered configuration for the module. + #.Description + # Imports the DefaultPath Configuration file, and then imports the Machine, Roaming (enterprise), and local config files, if they exist. + # Each configuration file is layered on top of the one before (so only needs to set values which are different) + #.Example + # $Configuration = Import-Configuration + # + # This example shows how to use Import-Configuration in your module to load the cached data + # + #.Example + # $Configuration = Get-Module Configuration | Import-Configuration + # + # This example shows how to use Import-Configuration in your module to load data cached for another module + # + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Callstack', Justification = 'This is referenced in ParameterBinder')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Module', Justification = 'This is referenced in ParameterBinder')] + # [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'DefaultPath', Justification = 'This is referenced in ParameterBinder')] + [CmdletBinding(DefaultParameterSetName = '__CallStack')] + param( + # A callstack. You should not ever pass this. + # It is used to calculate the defaults for all the other parameters. + [Parameter(ParameterSetName = "__CallStack")] + [System.Management.Automation.CallStackFrame[]]$CallStack = $(Get-PSCallStack), + + # The Module you're importing configuration for + [Parameter(ParameterSetName = "__ModuleInfo", ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] + [System.Management.Automation.PSModuleInfo]$Module, + + # An optional module qualifier (by default, this is blank) + [Parameter(ParameterSetName = "ManualOverride", Mandatory = $true, ValueFromPipelineByPropertyName = $true)] + [Alias("Author")] + [String]$CompanyName, + + # The name of the module or script + # Will be used in the returned storage path + [Parameter(ParameterSetName = "ManualOverride", Mandatory = $true, ValueFromPipelineByPropertyName = $true)] + [String]$Name, + + # The full path (including file name) of a default Configuration.psd1 file + # By default, this is expected to be in the same folder as your module manifest, or adjacent to your script file + [Parameter(ParameterSetName = "ManualOverride", ValueFromPipelineByPropertyName = $true)] + [Alias("ModuleBase")] + [String]$DefaultPath, + + # The version for saved settings -- if set, will be used in the returned path + # NOTE: this is *never* calculated, if you use version numbers, you must manage them on your own + [Version]$Version, + + # If set (and PowerShell version 4 or later) preserve the file order of configuration + # This results in the output being an OrderedDictionary instead of Hashtable + [Switch]$Ordered, + + # Allows extending the valid variables which are allowed to be referenced in configuration + # BEWARE: This exposes the value of these variables in the calling context to the configuration file + # You are reponsible to only allow variables which you know are safe to share + [String[]]$AllowedVariables + ) + begin { + # Write-Debug "Import-Configuration for module $Name" + } + process { + . ParameterBinder + + if (!$Name) { + throw "Could not determine the configuration name. When you are not calling Import-Configuration from a module, you must specify the -Author and -Name parameter" + } + + $MetadataOptions = @{ + AllowedVariables = $AllowedVariables + PSVariable = $PSCmdlet.SessionState.PSVariable + Ordered = $Ordered + ErrorAction = "Ignore" + } + + if ($DefaultPath -and (Test-Path $DefaultPath -Type Container)) { + $DefaultPath = Join-Path $DefaultPath Configuration.psd1 + } + + $Configuration = if ($DefaultPath -and (Test-Path $DefaultPath)) { + Import-Metadata $DefaultPath @MetadataOptions + } else { + @{} + } + # Write-Debug "Module Configuration: ($DefaultPath)`n$($Configuration | Out-String)" + + + $Parameters = @{ + CompanyName = $CompanyName + Name = $Name + } + if ($Version) { + $Parameters.Version = $Version + } + + $MachinePath = Get-ConfigurationPath @Parameters -Scope Machine -SkipCreatingFolder + $MachinePath = Join-Path $MachinePath Configuration.psd1 + $Machine = if (Test-Path $MachinePath) { + Import-Metadata $MachinePath @MetadataOptions + } else { + @{} + } + # Write-Debug "Machine Configuration: ($MachinePath)`n$($Machine | Out-String)" + + + $EnterprisePath = Get-ConfigurationPath @Parameters -Scope Enterprise -SkipCreatingFolder + $EnterprisePath = Join-Path $EnterprisePath Configuration.psd1 + $Enterprise = if (Test-Path $EnterprisePath) { + Import-Metadata $EnterprisePath @MetadataOptions + } else { + @{} + } + # Write-Debug "Enterprise Configuration: ($EnterprisePath)`n$($Enterprise | Out-String)" + + $LocalUserPath = Get-ConfigurationPath @Parameters -Scope User -SkipCreatingFolder + $LocalUserPath = Join-Path $LocalUserPath Configuration.psd1 + $LocalUser = if (Test-Path $LocalUserPath) { + Import-Metadata $LocalUserPath @MetadataOptions + } else { + @{} + } + # Write-Debug "LocalUser Configuration: ($LocalUserPath)`n$($LocalUser | Out-String)" + + $Configuration | Update-Object $Machine | + Update-Object $Enterprise | + Update-Object $LocalUser + } +} +#EndRegion '.\Public\Import-Configuration.ps1' 130 +#Region '.\Public\Import-ParameterConfiguration.ps1' 0 +function Import-ParameterConfiguration { + <# + .SYNOPSIS + Loads a metadata file based on the calling command name and combines the values there with the parameter values of the calling function. + .DESCRIPTION + This function gives command authors and users an easy way to let the default parameter values of the command be set by a configuration file in the folder you call it from. + + Normally, you have three places to get parameter values from. In priority order, they are: + - Parameters passed by the caller always win + - The PowerShell $PSDefaultParameterValues hashtable appears to the function as if the user passed it + - Default parameter values (defined in the function) + + If you call this command at the top of a function, it overrides (only) the default parameter values with + + - Values from a manifest file in the present working directory ($pwd) + .EXAMPLE + Given that you've written a script like: + + function New-User { + [CmdletBinding()] + param( + $FirstName, + $LastName, + $UserName, + $Domain, + $EMail, + $Department, + [hashtable]$Permissions + ) + Import-ParameterConfiguration -Recurse + # Possibly calculated based on (default) parameter values + if (-not $UserName) { $UserName = "$FirstName.$LastName" } + if (-not $EMail) { $EMail = "$UserName@$Domain" } + + # Lots of work to create the user's AD account, email, set permissions etc. + + # Output an object: + [PSCustomObject]@{ + PSTypeName = "MagicUser" + FirstName = $FirstName + LastName = $LastName + EMail = $EMail + Department = $Department + Permissions = $Permissions + } + } + + You could create a User.psd1 in a folder with just: + + @{ Domain = "HuddledMasses.org" } + + Now the following command would resolve the `User.psd1` + And the user would get an appropriate email address automatically: + + PS> New-User Joel Bennett + + FirstName : Joel + LastName : Bennett + EMail : Joel.Bennett@HuddledMasses.org + + .EXAMPLE + Import-ParameterConfiguration works recursively (up through parent folders) + + That means it reads config files in the same way git reads .gitignore, + with settings in the higher level files (up to the root?) being + overridden by those in lower level files down to the WorkingDirectory + + Following the previous example to a ridiculous conclusion, + we could automate creating users by creating a tree like: + + C:\HuddledMasses\Security\Admins\ with a User.psd1 in each folder: + + # C:\HuddledMasses\User.psd1: + @{ + Domain = "HuddledMasses.org" + } + + # C:\HuddledMasses\Security\User.psd1: + @{ + Department = "Security" + Permissions = @{ + Access = "User" + } + } + + # C:\HuddledMasses\Security\Admins\User.psd1 + @{ + Permissions = @{ + Access = "Administrator" + } + } + + And then switch to the Admins directory and run: + + PS> New-User Joel Bennett + + FirstName : Joel + LastName : Bennett + EMail : Joel.Bennett@HuddledMasses.org + Department : Security + Permissions : { Access = Administrator } + + .EXAMPLE + Following up on our earlier example, let's look at a way to use that -FileName parameter. + If you wanted to use a different configuration files than your Noun, you can pass the file name in. + + You could even use one of your parameters to generate the file name. If we modify the function like ... + + function New-User { + [CmdletBinding()] + param( + $FirstName, + $LastName, + $UserName, + $Domain, + $EMail, + $Department, + [hashtable]$Permissions + ) + Import-ParameterConfiguration -FileName "${Department}User.psd1" + # Possibly calculated based on (default) parameter values + if (-not $UserName) { $UserName = "$FirstName.$LastName" } + if (-not $EMail) { $EMail = "$UserName@$Domain" } + + # Lots of work to create the user's AD account and email etc. + [PSCustomObject]@{ + PSTypeName = "MagicUser" + FirstName = $FirstName + LastName = $LastName + EMail = $EMail + # Passthru for testing + Permissions = $Permissions + } + } + + Now you could create a `SecurityUser.psd1` + + @{ + Domain = "HuddledMasses.org" + Permissions = @{ + Access = "Administrator" + } + } + + And run: + + PS> New-User Joel Bennett -Department Security + #> + [CmdletBinding()] + param( + # The folder the configuration should be read from. Defaults to the current working directory + [string]$WorkingDirectory = $pwd, + # The name of the configuration file. + # The default value is your command's Noun, with the ".psd1" extention. + # So if you call this from a command named Build-Module, the noun is "Module" and the config $FileName is "Module.psd1" + [string]$FileName, + + # If set, considers configuration files in the parent, and it's parent recursively + [switch]$Recurse, + + # Allows extending the valid variables which are allowed to be referenced in configuration + # BEWARE: This exposes the value of these variables in the calling context to the configuration file + # You are reponsible to only allow variables which you know are safe to share + [String[]]$AllowedVariables + ) + + $CallersInvocation = $PSCmdlet.SessionState.PSVariable.GetValue("MyInvocation") + $BoundParameters = @{} + $CallersInvocation.BoundParameters + $AllParameters = $CallersInvocation.MyCommand.Parameters.Keys + if (-not $PSBoundParameters.ContainsKey("FileName")) { + $FileName = "$($CallersInvocation.MyCommand.Noun).psd1" + } + + $MetadataOptions = @{ + AllowedVariables = $AllowedVariables + PSVariable = $PSCmdlet.SessionState.PSVariable + ErrorAction = "SilentlyContinue" + } + + do { + $FilePath = Join-Path $WorkingDirectory $FileName + + Write-Debug "Initializing parameters for $($CallersInvocation.InvocationName) from $(Join-Path $WorkingDirectory $FileName)" + if (Test-Path $FilePath) { + $ConfiguredDefaults = Import-Metadata $FilePath @MetadataOptions + + foreach ($Parameter in $AllParameters) { + # If it's in the defaults AND it was not already set at a higher precedence + if ($ConfiguredDefaults.ContainsKey($Parameter) -and -not ($BoundParameters.ContainsKey($Parameter))) { + Write-Debug "Export $Parameter = $($ConfiguredDefaults[$Parameter])" + $BoundParameters.Add($Parameter, $ConfiguredDefaults[$Parameter]) + # This "SessionState" is the _callers_ SessionState, not ours + $PSCmdlet.SessionState.PSVariable.Set($Parameter, $ConfiguredDefaults[$Parameter]) + } + } + } + Write-Debug "Recurse:$Recurse -and $($BoundParameters.Count) of $($AllParameters.Count) Parameters and $WorkingDirectory" + } while ($Recurse -and ($AllParameters.Count -gt $BoundParameters.Count) -and ($WorkingDirectory = Split-Path $WorkingDirectory)) +} +#EndRegion '.\Public\Import-ParameterConfiguration.ps1' 200 diff --git a/Tools/Configuration/1.6.0/PSGetModuleInfo.xml b/Tools/Configuration/1.6.0/PSGetModuleInfo.xml new file mode 100644 index 000000000000..5aeaa96f694c --- /dev/null +++ b/Tools/Configuration/1.6.0/PSGetModuleInfo.xml @@ -0,0 +1,162 @@ + + + + Microsoft.PowerShell.Commands.PSRepositoryItemInfo + System.Management.Automation.PSCustomObject + System.Object + + + Configuration + 1.6.0 + Module + A module for storing and reading configuration values, with full PS Data serialization, automatic configuration for modules and scripts, etc. + Joel Bennett + Jaykul + Copyright (c) 2014-2021 by Joel Bennett, all rights reserved. +
2023-08-24T04:24:42+08:00
+ +
2025-12-08T22:53:41.8132697+08:00
+ + + + Microsoft.PowerShell.Commands.DisplayHintType + System.Enum + System.ValueType + System.Object + + DateTime + 2 + + +
+ + http://opensource.org/licenses/MIT + https://github.com/PoshCode/Configuration + + + + System.Object[] + System.Array + System.Object + + + Development + Configuration + Settings + Storage + PSModule + + + + + System.Collections.Hashtable + System.Object + + + + Workflow + + + + + + + Cmdlet + + + + Command + + + + Export-Configuration + Get-ConfigurationPath + Get-ParameterValue + Import-Configuration + Import-ParameterConfiguration + + + + + Function + + + + Export-Configuration + Get-ConfigurationPath + Get-ParameterValue + Import-Configuration + Import-ParameterConfiguration + + + + + DscResource + + + + RoleCapability + + + + + + - Extract the Metadata module_x000D__x000A_ - Add support for arbitrary AllowedVariables + + + + + + System.Collections.Specialized.OrderedDictionary + System.Object + + + + Name + Metadata + + + CanonicalId + nuget:Metadata + + + + + + https://www.powershellgallery.com/api/v2 + PSGallery + NuGet + + + System.Management.Automation.PSCustomObject + System.Object + + + Copyright (c) 2014-2021 by Joel Bennett, all rights reserved. + A module for storing and reading configuration values, with full PS Data serialization, automatic configuration for modules and scripts, etc. + False + - Extract the Metadata module_x000D__x000A_ - Add support for arbitrary AllowedVariables + True + True + 550723 + 925498 + 11586 + 24/08/2023 4:24:42 AM +08:00 + 24/08/2023 4:24:42 AM +08:00 + 8/12/2025 2:50:00 PM +08:00 + Development Configuration Settings Storage PSModule PSFunction_Export-Configuration PSCommand_Export-Configuration PSFunction_Get-ConfigurationPath PSCommand_Get-ConfigurationPath PSFunction_Get-ParameterValue PSCommand_Get-ParameterValue PSFunction_Import-Configuration PSCommand_Import-Configuration PSFunction_Import-ParameterConfiguration PSCommand_Import-ParameterConfiguration PSIncludes_Function + False + 2025-12-08T14:50:00Z + 1.6.0 + Joel Bennett + false + Module + Configuration.nuspec|Configuration.psd1|Configuration.psm1 + e56e5bec-4d97-4dfd-b138-abbaa14464a6 + HuddledMasses.org + + + C:\Users\Zac\Documents\PowerShell\Modules\Configuration\1.6.0 +
+
+
diff --git a/Tools/Metadata/1.5.7/Metadata.psd1 b/Tools/Metadata/1.5.7/Metadata.psd1 new file mode 100644 index 000000000000..a8656a191695 --- /dev/null +++ b/Tools/Metadata/1.5.7/Metadata.psd1 @@ -0,0 +1,50 @@ +@{ + +# Script module or binary module file associated with this manifest. +ModuleToProcess = 'Metadata.psm1' + +# Version number of this module. +ModuleVersion = '1.5.7' + +# ID used to uniquely identify this module +GUID = 'c7505d40-646d-46b5-a440-8a81791c5d23' + +# Author of this module +Author = @('Joel Bennett') + +# Company or vendor of this module +CompanyName = 'HuddledMasses.org' + +# Copyright statement for this module +Copyright = 'Copyright (c) 2014-2021 by Joel Bennett, all rights reserved.' + +# Description of the functionality provided by this module +Description = 'A module for PowerShell data serialization' + +# This doesn't make it into the build output, so it's irrelevant +FunctionsToExport = @('Add-MetadataConverter','ConvertFrom-Metadata','ConvertTo-Metadata','Export-Metadata','Get-Metadata','Import-Metadata','Test-PSVersion','Update-Metadata','Update-Object') +CmdletsToExport = @() +VariablesToExport = @() +AliasesToExport = @('FromMetadata','ToMetadata','Get-ManifestValue','Update-Manifest') +PrivateData = @{ + PSData = @{ + # The semver pre-release version information + PreRelease = '' + + # Keyword tags to help users find this module via navigations and search. + Tags = @('Serialization', 'Metadata', 'Development', 'Configuration', 'Settings') + + # The web address of this module's project or support homepage. + ProjectUri = "https://github.com/PoshCode/Metadata" + + # The web address of this module's license. Points to a page that's embeddable and linkable. + LicenseUri = "http://opensource.org/licenses/MIT" + + # Release notes for this particular version of the module + ReleaseNotes = ' + Fixes PSObject serialization bug + ' + } +} + +} diff --git a/Tools/Metadata/1.5.7/Metadata.psm1 b/Tools/Metadata/1.5.7/Metadata.psm1 new file mode 100644 index 000000000000..262c1ae161d5 --- /dev/null +++ b/Tools/Metadata/1.5.7/Metadata.psm1 @@ -0,0 +1,1210 @@ +#Region '.\Header\00. param.ps1' 0 +param( + $Converters = @{} +) + +$ModuleManifestExtension = ".psd1" +#EndRegion '.\Header\00. param.ps1' 6 +#Region '.\Header\01. IPsMetadataSerializable.ps1' 0 +Add-Type -TypeDefinition @' +public interface IPsMetadataSerializable { + string ToPsMetadata(); + void FromPsMetadata(string Metadata); +} +'@ +#EndRegion '.\Header\01. IPsMetadataSerializable.ps1' 7 +#Region '.\Private\FindHashKeyValue.ps1' 0 +function FindHashKeyValue { + [CmdletBinding()] + param( + $SearchPath, + $Ast, + [string[]] + $CurrentPath = @() + ) + # Write-Debug "FindHashKeyValue: $SearchPath -eq $($CurrentPath -Join '.')" + if ($SearchPath -eq ($CurrentPath -Join '.') -or $SearchPath -eq $CurrentPath[-1]) { + return $Ast | + Add-Member NoteProperty HashKeyPath ($CurrentPath -join '.') -PassThru -Force | + Add-Member NoteProperty HashKeyName ($CurrentPath[-1]) -PassThru -Force + } + + if ($Ast.PipelineElements.Expression -is [System.Management.Automation.Language.HashtableAst] ) { + $KeyValue = $Ast.PipelineElements.Expression + foreach ($KV in $KeyValue.KeyValuePairs) { + $result = FindHashKeyValue $SearchPath -Ast $KV.Item2 -CurrentPath ($CurrentPath + $KV.Item1.Value) + if ($null -ne $result) { + $result + } + } + } +} +#EndRegion '.\Private\FindHashKeyValue.ps1' 26 +#Region '.\Private\ThrowError.ps1' 0 +# Utility to throw an errorrecord +function ThrowError { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidOverwritingBuiltInCmdlets", "")] + param + ( + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCmdlet] + $Cmdlet = $((Get-Variable -Scope 1 PSCmdlet).Value), + + [Parameter(Mandatory = $true, ParameterSetName = "ExistingException", Position = 1, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] + [Parameter(ParameterSetName = "NewException")] + [ValidateNotNullOrEmpty()] + [System.Exception] + $Exception, + + [Parameter(ParameterSetName = "NewException", Position = 2)] + [ValidateNotNullOrEmpty()] + [System.String] + $ExceptionType = "System.Management.Automation.RuntimeException", + + [Parameter(Mandatory = $true, ParameterSetName = "NewException", Position = 3)] + [ValidateNotNullOrEmpty()] + [System.String] + $Message, + + [Parameter(Mandatory = $false)] + [System.Object] + $TargetObject, + + [Parameter(Mandatory = $true, ParameterSetName = "ExistingException", Position = 10)] + [Parameter(Mandatory = $true, ParameterSetName = "NewException", Position = 10)] + [ValidateNotNullOrEmpty()] + [System.String] + $ErrorId, + + [Parameter(Mandatory = $true, ParameterSetName = "ExistingException", Position = 11)] + [Parameter(Mandatory = $true, ParameterSetName = "NewException", Position = 11)] + [ValidateNotNull()] + [System.Management.Automation.ErrorCategory] + $Category, + + [Parameter(Mandatory = $true, ParameterSetName = "Rethrow", Position = 1)] + [System.Management.Automation.ErrorRecord]$ErrorRecord + ) + process { + if (!$ErrorRecord) { + if ($PSCmdlet.ParameterSetName -eq "NewException") { + if ($Exception) { + $Exception = New-Object $ExceptionType $Message, $Exception + } else { + $Exception = New-Object $ExceptionType $Message + } + } + $errorRecord = New-Object System.Management.Automation.ErrorRecord $Exception, $ErrorId, $Category, $TargetObject + } + $Cmdlet.ThrowTerminatingError($errorRecord) + } +} +#EndRegion '.\Private\ThrowError.ps1' 60 +#Region '.\Private\WriteError.ps1' 0 +# Utility to throw an errorrecord +function WriteError { + param + ( + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCmdlet] + $Cmdlet = $((Get-Variable -Scope 1 PSCmdlet).Value), + + [Parameter(Mandatory = $true, ParameterSetName = "ExistingException", Position = 1, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] + [Parameter(ParameterSetName = "NewException")] + [ValidateNotNullOrEmpty()] + [System.Exception] + $Exception, + + [Parameter(ParameterSetName = "NewException", Position = 2)] + [ValidateNotNullOrEmpty()] + [System.String] + $ExceptionType = "System.Management.Automation.RuntimeException", + + [Parameter(Mandatory = $true, ParameterSetName = "NewException", Position = 3)] + [ValidateNotNullOrEmpty()] + [System.String] + $Message, + + [Parameter(Mandatory = $false)] + [System.Object] + $TargetObject, + + [Parameter(Mandatory = $true, Position = 10)] + [ValidateNotNullOrEmpty()] + [System.String] + $ErrorId, + + [Parameter(Mandatory = $true, Position = 11)] + [ValidateNotNull()] + [System.Management.Automation.ErrorCategory] + $Category, + + [Parameter(Mandatory = $true, ParameterSetName = "Rethrow", Position = 1)] + [System.Management.Automation.ErrorRecord]$ErrorRecord + ) + process { + if (!$ErrorRecord) { + if ($PSCmdlet.ParameterSetName -eq "NewException") { + if ($Exception) { + $Exception = New-Object $ExceptionType $Message, $Exception + } else { + $Exception = New-Object $ExceptionType $Message + } + } + $errorRecord = New-Object System.Management.Automation.ErrorRecord $Exception, $ErrorId, $Category, $TargetObject + } + $Cmdlet.WriteError($errorRecord) + } +} +#EndRegion '.\Private\WriteError.ps1' 57 +#Region '.\Public\Add-MetadataConverter.ps1' 0 +function Add-MetadataConverter { + <# + .Synopsis + Add a converter functions for serialization and deserialization to metadata + .Description + Add-MetadataConverter allows you to map: + * a type to a scriptblock which can serialize that type to metadata (psd1) + * define a name and scriptblock as a function which will be whitelisted in metadata (for ConvertFrom-Metadata and Import-Metadata) + + The idea is to give you a way to extend the serialization capabilities if you really need to. + .Example + Add-MetadataCOnverter @{ [bool] = { if($_) { '$True' } else { '$False' } } } + + Shows a simple example of mapping bool to a scriptblock that serializes it in a way that's inherently parseable by PowerShell. This exact converter is already built-in to the Metadata module, so you don't need to add it. + + .Example + Add-MetadataConverter @{ + [Uri] = { "Uri '$_' " } + "Uri" = { + param([string]$Value) + [Uri]$Value + } + } + + Shows how to map a function for serializing Uri objects as strings with a Uri function that just casts them. Normally you wouldn't need to do that for Uri, since they output strings natively, and it's perfectly logical to store Uris as strings and only cast them when you really need to. + + .Example + Add-MetadataConverter @{ + [DateTimeOffset] = { "DateTimeOffset {0} {1}" -f $_.Ticks, $_.Offset } + "DateTimeOffset" = {param($ticks,$offset) [DateTimeOffset]::new( $ticks, $offset )} + } + + Shows how to change the DateTimeOffset serialization. + + By default, DateTimeOffset values are (de)serialized using the 'o' RoundTrips formatting + e.g.: [DateTimeOffset]::Now.ToString('o') + + #> + [CmdletBinding()] + param( + # A hashtable of types to serializer scriptblocks, or function names to scriptblock definitions + [Parameter(Mandatory = $True)] + [hashtable]$Converters + ) + + if ($Converters.Count) { + switch ($Converters.Keys.GetEnumerator()) { + {$Converters[$_] -isnot [ScriptBlock]} { + WriteError -ExceptionType System.ArgumentExceptionn ` + -Message "Ignoring $_ converter, the value must be ScriptBlock!" ` + -ErrorId "NotAScriptBlock,Metadata\Add-MetadataConverter" ` + -Category "InvalidArgument" + continue + } + + {$_ -is [String]} { + # Write-Debug "Storing deserialization function: $_" + Set-Content "function:script:$_" $Converters[$_] + # We need to store the function in MetadataDeserializers + $script:MetadataDeserializers[$_] = $Converters[$_] + continue + } + + {$_ -is [Type]} { + # Write-Debug "Adding serializer for $($_.FullName)" + $script:MetadataSerializers[$_] = $Converters[$_] + continue + } + default { + WriteError -ExceptionType System.ArgumentExceptionn ` + -Message "Unsupported key type in Converters: $_ is $($_.GetType())" ` + -ErrorId "InvalidKeyType,Metadata\Add-MetadataConverter" ` + -Category "InvalidArgument" + } + } + } +} +#EndRegion '.\Public\Add-MetadataConverter.ps1' 78 +#Region '.\Public\ConvertFrom-Metadata.ps1' 0 +function ConvertFrom-Metadata { + <# + .Synopsis + Deserializes objects from PowerShell Data language (PSD1) + .Description + Converts psd1 notation to actual objects, and supports passing in additional converters + in addition to using the built-in registered converters (see Add-MetadataConverter). + + NOTE: Any Converters that are passed in are temporarily added as though passed Add-MetadataConverter + .Example + ConvertFrom-Metadata 'PSObject @{ Name = PSObject @{ First = "Joel"; Last = "Bennett" }; Id = 1; }' + + Id Name + -- ---- + 1 @{Last=Bennett; First=Joel} + + Convert the example string into a real PSObject using the built-in object serializer. + .Example + $data = ConvertFrom-Metadata .\Configuration.psd1 -Ordered + + Convert a module manifest into a hashtable of properties for introspection, preserving the order in the file + .Example + ConvertFrom-Metadata ("DateTimeOffset 635968680686066846 -05:00:00") -Converters @{ + "DateTimeOffset" = { + param($ticks,$offset) + [DateTimeOffset]::new( $ticks, $offset ) + } + } + + Shows how to temporarily add a "ValidCommand" called "DateTimeOffset" to support extra data types in the metadata. + + See also the third example on ConvertTo-Metadata and Add-MetadataConverter + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "", Justification = "Too late to call it Metadatum, LOL")] + [Alias('FromMetadata')] + [CmdletBinding(DefaultParameterSetName = "Legacy")] + param( + # The metadata text (or a path to a metadata file) + [Parameter(Mandatory = $true, Position = 0, ValueFromPipelineByPropertyName = "True")] + [Alias("PSPath")] + $InputObject, + + # The Type that the InputObject represents + [Parameter(Mandatory = $true, Position = 1, ParameterSetName = "FromPsMetadata")] + [Alias("As")] + [Type]$Type, + + # A hashtable of MetadataConverters (same as with Add-MetadataConverter) + [Hashtable]$Converters = @{}, + + # The PSScriptRoot which the metadata should be evaluated from. + # You do not normally need to pass this, and it has no effect unless + # you're referencing $ScriptRoot in your metadata + $ScriptRoot = "$PSScriptRoot", + + # If set (and PowerShell version 4 or later) preserve the file order of configuration + # This results in the output being an OrderedDictionary instead of Hashtable + [Switch]$Ordered, + + # Allows extending the valid variables which are allowed to be referenced in metadata + # BEWARE: This exposes the value of these variables in your context to the caller + # You are reponsible to only allow variables which you know are safe to share + [String[]]$AllowedVariables, + + # You should not pass this. + # The PSVariable parameter is for preserving variable scope within the Metadata commands + [System.Management.Automation.PSVariableIntrinsics]$PSVariable + ) + begin { + $OriginalMetadataSerializers = $Script:MetadataSerializers.Clone() + $OriginalMetadataDeserializers = $Script:MetadataDeserializers.Clone() + if ($Converters.Count) { + Add-MetadataConverter $Converters + } + [string[]]$ValidCommands = @( + "ConvertFrom-StringData", "ConvertFrom-Metadata", "Join-Path", "Split-Path", "ConvertTo-SecureString" + ) + @($MetadataDeserializers.Keys) + [string[]]$ValidVariables = $AllowedVariables + @( + "PSScriptRoot", "ScriptRoot", "PoshCodeModuleRoot", "PSCulture", "PSUICulture", "True", "False", "Null" + ) + + if (!$PSVariable) { + $PSVariable = $PSCmdlet.SessionState.PSVariable + } + } + end { + $Script:MetadataSerializers = $OriginalMetadataSerializers.Clone() + $Script:MetadataDeserializers = $OriginalMetadataDeserializers.Clone() + } + process { + $ErrorActionPreference = "Stop" + $Tokens = $Null; $ParseErrors = $Null + # Write-Debug "ParameterSet: $($PSCmdlet.ParameterSetName) $($PSBoundParameters | Format-Table | Out-String)" + if ($Type) { + try { + if ($Type.GetMethod("FromPsMetadata", [string])) { + $Output = New-Object $Type + $Output.FromPsMetadata($InputObject) + $Output + return + } else { + $Output = New-Object $Type -Property $InputObject + return + } + } catch { + # I almost want to suppress this and just try the fallback methods + Write-Warning "$($Type.FullName) failed using FromPsMetadata" + } + } + + if (Test-PSVersion -lt "3.0") { + # Write-Debug "ConvertFrom-Metadata: Using Import-LocalizedData to support PowerShell $($PSVersionTable.PSVersion)" + # Write-Debug "ConvertFrom-Metadata: $InputObject" + if (!(Test-Path $InputObject -ErrorAction SilentlyContinue)) { + $Path = [IO.path]::ChangeExtension([IO.Path]::GetTempFileName(), $ModuleManifestExtension) + Set-Content -Encoding UTF8 -Path $Path $InputObject + $InputObject = $Path + } elseif (!"$InputObject".EndsWith($ModuleManifestExtension)) { + $Path = [IO.path]::ChangeExtension([IO.Path]::GetTempFileName(), $ModuleManifestExtension) + Copy-Item "$InputObject" "$Path" + $InputObject = $Path + } + $Result = $null + Import-LocalizedData -BindingVariable Result -BaseDirectory (Split-Path $InputObject) -FileName (Split-Path $InputObject -Leaf) -SupportedCommand $ValidCommands + return $Result + } + + if (Test-Path $InputObject -ErrorAction SilentlyContinue) { + # Write-Debug "ConvertFrom-Metadata: Using ParseInput to support PowerShell $($PSVersionTable.PSVersion)" + # ParseFile on PS5 (and older) doesn't handle utf8 encoding properly (treats it as ASCII) + # Sometimes, that causes an avoidable error. So I'm avoiding it, if I can: + $Path = Convert-Path $InputObject + if (!$PSBoundParameters.ContainsKey('ScriptRoot')) { + $ScriptRoot = Split-Path $Path + } + $Content = (Get-Content -Path $InputObject -Encoding UTF8) + # Remove SIGnature blocks, PowerShell doesn't parse them in .psd1 and chokes on them here. + $Content = $Content -join "`n" -replace "# SIG # Begin signature block(?s:.*)" + try { + # But older versions of PowerShell, this will throw a MethodException because the overload is missing + $AST = [System.Management.Automation.Language.Parser]::ParseInput($Content, $Path, [ref]$Tokens, [ref]$ParseErrors) + } catch [System.Management.Automation.MethodException] { + # Write-Debug "ConvertFrom-Metadata: Using ParseFile as a backup for PowerShell $($PSVersionTable.PSVersion)" + $AST = [System.Management.Automation.Language.Parser]::ParseFile( $Path, [ref]$Tokens, [ref]$ParseErrors) + + # If we got parse errors on older versions of PowerShell, test to see if the error is just encoding + if ($null -ne $ParseErrors -and $ParseErrors.Count -gt 0) { + $StillErrors = $null + $AST = [System.Management.Automation.Language.Parser]::ParseInput($Content, [ref]$Tokens, [ref]$StillErrors) + # If we didn't get errors, clear the errors + # Otherwise, we want to use the original errors with the path in them + if ($null -eq $StillErrors -or $StillErrors.Count -eq 0) { + $ParseErrors = $StillErrors + } + } + } + } else { + # Write-Debug "ConvertFrom-Metadata: Using ParseInput with loose metadata: $InputObject" + if (!$PSBoundParameters.ContainsKey('ScriptRoot')) { + $ScriptRoot = $PoshCodeModuleRoot + } + + $OFS = "`n" + # Remove SIGnature blocks, PowerShell doesn't parse them in .psd1 and chokes on them here. + $InputObject = "$InputObject" -replace "# SIG # Begin signature block(?s:.*)" + $AST = [System.Management.Automation.Language.Parser]::ParseInput($InputObject, [ref]$Tokens, [ref]$ParseErrors) + } + + if ($null -ne $ParseErrors -and $ParseErrors.Count -gt 0) { + ThrowError -Exception (New-Object System.Management.Automation.ParseException (, [System.Management.Automation.Language.ParseError[]]$ParseErrors)) -ErrorId "Metadata Error" -Category "ParserError" -TargetObject $InputObject + } + + # Get the variables or subexpressions from strings which have them ("StringExpandable" vs "String") ... + $Tokens += $Tokens | Where-Object { "StringExpandable" -eq $_.Kind } | Select-Object -ExpandProperty NestedTokens + + # Write-Debug "ConvertFrom-Metadata: Searching $($Tokens.Count) variables for: $($ValidVariables -join ', '))" + # Work around PowerShell rules about magic variables + # Change all the "ValidVariables" to use names like __Metadata__OriginalName__ + # Later, we'll try to make sure these are all set! + if (($UsedVariables = $Tokens | Where-Object { ("Variable" -eq $_.Kind) -and ($_.Name -in $ValidVariables) -and ($_.Name -notin "PSCulture", "PSUICulture", "True", "False", "Null") })) { + # Write-Debug "ConvertFrom-Metadata: Replacing $($UsedVariables.Name -join ', ')" + if (($extents = @( $UsedVariables | ForEach-Object { $_.Extent | Add-Member NoteProperty Name $_.Name -PassThru } ))) { + $ScriptContent = $Ast.ToString() + # Write-Debug "ConvertFrom-Metadata: Replacing $($UsedVariables.Count) variables in metadata: $ScriptContent" + for ($r = $extents.count - 1; $r -ge 0; $r--) { + $VariableExtent = $extents[$r] + $VariableName = if ($VariableExtent.Name -eq "PSScriptRoot") { + '${__Metadata__ScriptRoot__}' + } else { + '${__Metadata__' + $VariableExtent.Name + '__}' + } + $ScriptContent = $ScriptContent.Remove( $VariableExtent.StartOffset, + ($VariableExtent.EndOffset - $VariableExtent.StartOffset) + ).Insert($VariableExtent.StartOffset, $VariableName) + } + } + # Write-Debug "ConvertFrom-Metadata: Replaced $($UsedVariables.Name -join ' and ') in metadata: $ScriptContent" + $AST = [System.Management.Automation.Language.Parser]::ParseInput($ScriptContent, [ref]$Tokens, [ref]$ParseErrors) + } + + $Script = $AST.GetScriptBlock() + try { + [string[]]$PrivateVariables = $ValidVariables -replace "^.*$", '__Metadata__$0__' + # Write-Debug "ConvertFrom-Metadata: Validating metadata: $Script against $PrivateVariables" + $Script.CheckRestrictedLanguage( $ValidCommands, $PrivateVariables, $true ) + } catch { + ThrowError -Exception $_.Exception.InnerException -ErrorId "Metadata Error" -Category "InvalidData" -TargetObject $Script + } + + # Set the __Metadata__ValidVariables__ in our scope but not for constant variables: + $ReplacementVariables = $ValidVariables | Where-Object { $_ -notin "PSCulture", "PSUICulture", "True", "False", "Null" } + foreach ($name in $ReplacementVariables) { + # We already read the script root from the calling scope ... + if ($Name -in "PSScriptRoot", "ScriptRoot", "PoshCodeModuleRoot") { + $Value = $ScriptRoot + } elseif (!($Value = $PSVariable.GetValue($Name))) { + $Value = "`${$Name}" + } + # Write-Debug "ConvertFrom-Metadata: Setting __Metadata__${Name}__ = $Value" + Set-Variable "__Metadata__${Name}__" $Value + } + + if ($Ordered -and (Test-PSVersion -gt "3.0")) { + # Write-Debug "ConvertFrom-Metadata: Supporting [Ordered] on PowerShell $($PSVersionTable.PSVersion)" + # Make all the hashtables ordered, so that the output objects make more sense to humans... + if ($Tokens | Where-Object { "AtCurly" -eq $_.Kind }) { + $ScriptContent = $AST.ToString() + $Hashtables = $AST.FindAll( {$args[0] -is [System.Management.Automation.Language.HashtableAst] -and ("ordered" -ne $args[0].Parent.Type.TypeName)}, $Recurse) + $Hashtables = $Hashtables | ForEach-Object { + New-Object PSObject -Property @{Type = "([ordered]"; Position = $_.Extent.StartOffset} + New-Object PSObject -Property @{Type = ")"; Position = $_.Extent.EndOffset} + } | Sort-Object Position -Descending + foreach ($point in $Hashtables) { + $ScriptContent = $ScriptContent.Insert($point.Position, $point.Type) + } + + $AST = [System.Management.Automation.Language.Parser]::ParseInput($ScriptContent, [ref]$Tokens, [ref]$ParseErrors) + $Script = $AST.GetScriptBlock() + } + } + # Write-Debug "ConvertFrom-Metadata: Metadata: $Script" + # Write-Debug "ConvertFrom-Metadata: Switching to RestrictedLanguage mode" + $Mode, $ExecutionContext.SessionState.LanguageMode = $ExecutionContext.SessionState.LanguageMode, "RestrictedLanguage" + + try { + $Script.InvokeReturnAsIs(@()) + } finally { + $ExecutionContext.SessionState.LanguageMode = $Mode + # Write-Debug "ConvertFrom-Metadata: Switching to $Mode mode" + } + } +} +#EndRegion '.\Public\ConvertFrom-Metadata.ps1' 253 +#Region '.\Public\ConvertTo-Metadata.ps1' 0 +function ConvertTo-Metadata { + #.Synopsis + # Serializes objects to PowerShell Data language (PSD1) + #.Description + # Converts objects to a texual representation that is valid in PSD1, + # using the built-in registered converters (see Add-MetadataConverter). + # + # NOTE: Any Converters that are passed in are temporarily added as though passed Add-MetadataConverter + #.Example + # $Name = @{ First = "Joel"; Last = "Bennett" } + # @{ Name = $Name; Id = 1; } | ConvertTo-Metadata + # + # @{ + # Id = 1 + # Name = @{ + # Last = 'Bennett' + # First = 'Joel' + # } + # } + # + # Convert input objects into a formatted string suitable for storing in a psd1 file. + #.Example + # Get-ChildItem -File | Select-Object FullName, *Utc, Length | ConvertTo-Metadata + # + # Convert complex custom types to dynamic PSObjects using Select-Object. + # + # ConvertTo-Metadata understands PSObjects automatically, so this allows us to proceed + # without a custom serializer for File objects, but the serialized data + # will not be a FileInfo or a DirectoryInfo, just a custom PSObject + #.Example + # ConvertTo-Metadata ([DateTimeOffset]::Now) -Converters @{ + # [DateTimeOffset] = { "DateTimeOffset {0} {1}" -f $_.Ticks, $_.Offset } + # } + # + # Shows how to temporarily add a MetadataConverter to convert a specific type while serializing the current DateTimeOffset. + # Note that this serialization would require a "DateTimeOffset" function to exist in order to deserialize properly. + # + # See also the third example on ConvertFrom-Metadata and Add-MetadataConverter. + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "", Justification = "Too late to call it Metadatum, LOL")] + [Alias('ToMetadata')] + [OutputType([string])] + [CmdletBinding()] + param( + # The object to convert to metadata + [Parameter(ValueFromPipeline = $True)] + $InputObject, + + # Serialize objects as hashtables + [switch]$AsHashtable, + + # Additional converters + [Hashtable]$Converters = @{} + ) + begin { + if ($t -is [string] -and [string]::IsNullOrWhiteSpace($t)) { + # DO NOT USE += BECAUSE POWERSHELL WILL OPTIMIZE THIS AWAY + $t = $t + " " + } else { + $t = " " + } + $Script:OriginalMetadataSerializers = $Script:MetadataSerializers.Clone() + $Script:OriginalMetadataDeserializers = $Script:MetadataDeserializers.Clone() + Add-MetadataConverter $Converters + } + end { + $Script:MetadataSerializers = $Script:OriginalMetadataSerializers.Clone() + $Script:MetadataDeserializers = $Script:OriginalMetadataDeserializers.Clone() + } + process { + if ($Null -eq $InputObject) { + '""' + return + } + + if ($InputObject -is [IPsMetadataSerializable] -or + ($InputObject.ToPsMetadata -is [System.Management.Automation.PSMethod] -and + $InputObject.FromPsMetadata -is [System.Management.Automation.PSMethod])) { + try { + $result = "(ConvertFrom-Metadata @'`n{1}`n'@ -As {0})" -f $InputObject.GetType().FullName, $InputObject.ToPsMetadata() + if ($result -is [string]) { + $result + return + } + } catch { + <# The way we handle this is to #> + Write-Warning "InputObject of type $($InputObject.GetType().FullName) looks IMetadataSerializable, but threw an exception." + } + } + + if ($InputObject -is [Int16] -or + $InputObject -is [Int32] -or + $InputObject -is [Int64] -or + $InputObject -is [Double] -or + $InputObject -is [Decimal] -or + $InputObject -is [Byte] ) { + "$InputObject" + } elseif ($InputObject -is [String]) { + "'{0}'" -f $InputObject.ToString().Replace("'", "''") + } elseif ($InputObject -is [Collections.IDictionary]) { + "@{{`n{0}`n$($t -replace " $")}}" -f ($( + ForEach ($key in @($InputObject.Keys)) { + if ("$key" -match '^([A-Za-z_]\w*|-?\d+\.?\d*)$') { + "$t$key = " + (ConvertTo-Metadata $InputObject[$key] -AsHashtable:$AsHashtable) + } else { + "$t'$key' = " + (ConvertTo-Metadata $InputObject[$key] -AsHashtable:$AsHashtable) + } + }) -split "`n" -join "`n") + } elseif ($InputObject -is [System.Collections.IEnumerable]) { + "@($($(ForEach($item in @($InputObject)) { $item | ConvertTo-Metadata -AsHashtable:$AsHashtable}) -join ","))" + } elseif($InputObject -is [System.Management.Automation.ScriptBlock]) { + # Escape single-quotes by doubling them: + "(ScriptBlock '{0}')" -f ("$InputObject" -replace "'", "''") + } elseif ($InputObject.GetType().FullName -eq 'System.Management.Automation.PSCustomObject') { + # NOTE: we can't put [ordered] here because we need support for PS v2, but it's ok, because we put it in at parse-time + $(if ($AsHashtable) { + "@{{`n$t{0}`n}}" + } else { + "(PSObject @{{`n$t{0}`n}} -TypeName '$($InputObject.PSTypeNames -join "','")')" + }) -f ($( + ForEach ($key in $InputObject | Get-Member -MemberType Properties | Select-Object -ExpandProperty Name) { + if ("$key" -match '^([A-Za-z_]\w*|-?\d+\.?\d*)$') { + "$key = " + (ConvertTo-Metadata $InputObject.($key) -AsHashtable:$AsHashtable) + } else { + "'$($key -replace "'","''")' = " + (ConvertTo-Metadata $InputObject.($key) -AsHashtable:$AsHashtable) + } + } + ) -split "`n" -join "`n") + } elseif ($MetadataSerializers.ContainsKey($InputObject.GetType())) { + $Str = ForEach-Object $MetadataSerializers.($InputObject.GetType()) -InputObject $InputObject + + [bool]$IsCommand = & { + $ErrorActionPreference = "Stop" + $Tokens = $Null; $ParseErrors = $Null + $AST = [System.Management.Automation.Language.Parser]::ParseInput( $Str, [ref]$Tokens, [ref]$ParseErrors) + $Null -ne $Ast.Find( {$args[0] -is [System.Management.Automation.Language.CommandAst]}, $false) + } + + if ($IsCommand) { "($Str)" } else { $Str } + } else { + Write-Warning "$($InputObject.GetType().FullName) is not serializable. Serializing as string" + "'{0}'" -f $InputObject.ToString().Replace("'", "`'`'") + } + } +} +#EndRegion '.\Public\ConvertTo-Metadata.ps1' 145 +#Region '.\Public\Export-Metadata.ps1' 0 +function Export-Metadata { + <# + .Synopsis + Creates a metadata file from a simple object + .Description + Serves as a wrapper for ConvertTo-Metadata to explicitly support exporting to files + + Note that exportable data is limited by the rules of data sections (see about_Data_Sections) and the available MetadataSerializers (see Add-MetadataConverter) + + The only things inherently importable in PowerShell metadata files are Strings, Booleans, and Numbers ... and Arrays or Hashtables where the values (and keys) are all strings, booleans, or numbers. + + Note: this function and the matching Import-Metadata are extensible, and have included support for PSCustomObject, Guid, Version, etc. + .Example + $Configuration | Export-Metadata .\Configuration.psd1 + + Export a configuration object (or hashtable) to the default Configuration.psd1 file for a module + the metadata module uses Configuration.psd1 as it's default config file. + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "", Justification = "Too late to call it Metadatum, LOL")] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "")] # Because PSSCriptAnalyzer team refuses to listen to reason. See bugs: #194 #283 #521 #608 + [CmdletBinding(SupportsShouldProcess)] + param( + # Specifies the path to the PSD1 output file. + [Parameter(Mandatory = $true, Position = 0)] + $Path, + + # comments to place on the top of the file (to explain settings or whatever for people who might edit it by hand) + [string[]]$CommentHeader, + + # Specifies the objects to export as metadata structures. + # Enter a variable that contains the objects or type a command or expression that gets the objects. + # You can also pipe objects to Export-Metadata. + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + $InputObject, + + # Serialize objects as hashtables + [switch]$AsHashtable, + + [Hashtable]$Converters = @{}, + + # If set, output the nuspec file + [Switch]$Passthru + ) + begin { + $data = @() + } + process { + $data += @($InputObject) + } + end { + # Avoid arrays when they're not needed: + if ($data.Count -eq 1) { + $data = $data[0] + } + Set-Content -Encoding UTF8 -Path $Path -Value ((@($CommentHeader) + @(ConvertTo-Metadata -InputObject $data -Converters $Converters -AsHashtable:$AsHashtable)) -Join "`n") + if ($Passthru) { + Get-Item $Path + } + } +} +#EndRegion '.\Public\Export-Metadata.ps1' 61 +#Region '.\Public\Get-Metadata.ps1' 0 +function Get-Metadata { + #.Synopsis + # Reads a specific value from a PowerShell metadata file (e.g. a module manifest) + #.Description + # By default Get-Metadata gets the ModuleVersion, but it can read any key in the metadata file + #.Example + # Get-Metadata .\Configuration.psd1 + # + # Returns the module version number (as a string) + #.Example + # Get-Metadata .\Configuration.psd1 ReleaseNotes + # + # Returns the release notes! + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "", Justification = "Too late to call it Metadatum, LOL")] + [Alias("Get-ManifestValue")] + [CmdletBinding()] + param( + # The path to the module manifest file + [Parameter(ValueFromPipelineByPropertyName = "True", Position = 0, Mandatory)] + [Alias("PSPath")] + [ValidateScript({ + if ([IO.Path]::GetExtension($_) -ne ".psd1") { + throw "Path must point to a .psd1 file" + } + $true + })] + [string]$Path, + + # The property (or dotted property path) to be read from the manifest. + # Get-Metadata searches the Manifest root properties, and also the nested hashtable properties. + [Parameter(ParameterSetName = "Overwrite", Position = 1)] + [string]$PropertyName = 'ModuleVersion', + + [switch]$Passthru + ) + process { + $ErrorActionPreference = "Stop" + + if (!(Test-Path $Path)) { + WriteError -ExceptionType System.Management.Automation.ItemNotFoundException ` + -Message "Can't find file $Path" ` + -ErrorId "PathNotFound,Metadata\Get-Metadata" ` + -Category "ObjectNotFound" + return + } + $Path = Convert-Path $Path + + $Tokens = $Null; $ParseErrors = $Null + $AST = [System.Management.Automation.Language.Parser]::ParseFile( $Path, [ref]$Tokens, [ref]$ParseErrors ) + + $KeyValue = $Ast.EndBlock.Statements + $KeyValue = @(FindHashKeyValue $PropertyName $KeyValue) + if ($KeyValue.Count -eq 0) { + WriteError -ExceptionType System.Management.Automation.ItemNotFoundException ` + -Message "Can't find '$PropertyName' in $Path" ` + -ErrorId "PropertyNotFound,Metadata\Get-Metadata" ` + -Category "ObjectNotFound" + return + } + if ($KeyValue.Count -gt 1) { + $SingleKey = @($KeyValue | Where-Object { $_.HashKeyPath -eq $PropertyName }) + + if ($SingleKey.Count -ne 1) { + WriteError -ExceptionType System.Reflection.AmbiguousMatchException ` + -Message ("Found more than one '$PropertyName' in $Path. Please specify a dotted path instead. Matching paths include: '{0}'" -f ($KeyValue.HashKeyPath -join "', '")) ` + -ErrorId "AmbiguousMatch,Metadata\Get-Metadata" ` + -Category "InvalidArgument" + return + } else { + $KeyValue = $SingleKey + } + } + $KeyValue = $KeyValue[0] + + if ($Passthru) { + $KeyValue + } else { + # # Write-Debug "Start $($KeyValue.Extent.StartLineNumber) : $($KeyValue.Extent.StartColumnNumber) (char $($KeyValue.Extent.StartOffset))" + # # Write-Debug "End $($KeyValue.Extent.EndLineNumber) : $($KeyValue.Extent.EndColumnNumber) (char $($KeyValue.Extent.EndOffset))" + + # In PowerShell 5+ we can just use: + if ($KeyValue.SafeGetValue) { + $KeyValue.SafeGetValue() + } else { + # Otherwise, this worked for simple values: + $Expression = $KeyValue.GetPureExpression() + if ($Expression.Value) { + $Expression.Value + } else { + # For complex (arrays, hashtables) we parse it ourselves + ConvertFrom-Metadata $KeyValue + } + } + } + } +} +#EndRegion '.\Public\Get-Metadata.ps1' 97 +#Region '.\Public\Import-Metadata.ps1' 0 +function Import-Metadata { + <# + .Synopsis + Creates a data object from the items in a Metadata file (e.g. a .psd1) + .Description + Serves as a wrapper for ConvertFrom-Metadata to explicitly support importing from files + .Example + $data = Import-Metadata .\Configuration.psd1 -Ordered + + Convert a module manifest into a hashtable of properties for introspection, preserving the order in the file + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "", Justification = "Too late to call it Metadatum, LOL")] + [CmdletBinding()] + param( + # The path to the metadata (.psd1) file to import + [Parameter(ValueFromPipeline = $true, Mandatory = $true, ValueFromPipelineByPropertyName = $true)] + [Alias("PSPath", "Content")] + [string]$Path, + + # A hashtable of MetadataConverters (same as with Add-MetadataConverter) + [Hashtable]$Converters = @{}, + + # If set (and PowerShell version 4 or later) preserve the file order of configuration + # This results in the output being an OrderedDictionary instead of Hashtable + [Switch]$Ordered, + + # Allows extending the valid variables which are allowed to be referenced in metadata + # BEWARE: This exposes the value of these variables in the calling context to the metadata file + # You are reponsible to only allow variables which you know are safe to share + [String[]]$AllowedVariables, + + # You should not pass this. + # The PSVariable parameter is for preserving variable scope within the Metadata commands + [System.Management.Automation.PSVariableIntrinsics]$PSVariable + ) + process { + if (!$PSVariable) { + $PSVariable = $PSCmdlet.SessionState.PSVariable + } + if (Test-Path $Path) { + # Write-Debug "Importing Metadata file from `$Path: $Path" + if (!(Test-Path $Path -PathType Leaf)) { + $Path = Join-Path $Path ((Split-Path $Path -Leaf) + $ModuleManifestExtension) + } + } + if (!(Test-Path $Path)) { + WriteError -ExceptionType System.Management.Automation.ItemNotFoundException ` + -Message "Can't find file $Path" ` + -ErrorId "PathNotFound,Metadata\Import-Metadata" ` + -Category "ObjectNotFound" + return + } + try { + ConvertFrom-Metadata -InputObject $Path -Converters $Converters -Ordered:$Ordered -AllowedVariables $AllowedVariables -PSVariable $PSVariable + } catch { + ThrowError $_ + } + } +} +#EndRegion '.\Public\Import-Metadata.ps1' 60 +#Region '.\Public\Test-PSVersion.ps1' 0 +function Test-PSVersion { + <# + .Synopsis + Test the PowerShell Version + .Description + This function exists so I can do things differently on older versions of PowerShell. + But the reason I test in a function is that I can mock the Version to test the alternative code. + .Example + if(Test-PSVersion -ge 3.0) { + ls | where Length -gt 12mb + } else { + ls | Where { $_.Length -gt 12mb } + } + + This is just a trivial example to show the usage (you wouldn't really bother for a where-object call) + #> + [OutputType([bool])] + [CmdletBinding()] + param( + [Version]$Version = $PSVersionTable.PSVersion, + [Version]$lt, + [Version]$le, + [Version]$gt, + [Version]$ge, + [Version]$eq, + [Version]$ne + ) + + $all = @( + if ($lt) { $Version -lt $lt } + if ($gt) { $Version -gt $gt } + if ($le) { $Version -le $le } + if ($ge) { $Version -ge $ge } + if ($eq) { $Version -eq $eq } + if ($ne) { $Version -ne $ne } + ) + + $all -notcontains $false +} +#EndRegion '.\Public\Test-PSVersion.ps1' 40 +#Region '.\Public\Update-Metadata.ps1' 0 +function Update-Metadata { + <# + .Synopsis + Update a single value in a PowerShell metadata file + .Description + By default Update-Metadata increments "ModuleVersion" + because my primary use of it is during builds, + but you can pass the PropertyName and Value for any key in a module Manifest, its PrivateData, or the PSData in PrivateData. + + NOTE: This will not currently create new keys, or uncomment keys. + .Example + Update-Metadata .\Configuration.psd1 + + Increments the Build part of the ModuleVersion in the Configuration.psd1 file + .Example + Update-Metadata .\Configuration.psd1 -Increment Major + + Increments the Major version part of the ModuleVersion in the Configuration.psd1 file + .Example + Update-Metadata .\Configuration.psd1 -Value '0.4' + + Sets the ModuleVersion in the Configuration.psd1 file to 0.4 + .Example + Update-Metadata .\Configuration.psd1 -Property ReleaseNotes -Value 'Add the awesome Update-Metadata function!' + + Sets the PrivateData.PSData.ReleaseNotes value in the Configuration.psd1 file! + #> + [Alias("Update-Manifest")] + # Because PSSCriptAnalyzer team refuses to listen to reason. See bugs: #194 #283 #521 #608 + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "")] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "", Justification = "Too late to call it Metadatum, LOL")] + [CmdletBinding(SupportsShouldProcess)] + param( + # The path to the module manifest file -- must be a .psd1 file + # As an easter egg, you can pass the CONTENT of a psd1 file instead, and the modified data will pass through + [Parameter(ValueFromPipelineByPropertyName = "True", Position = 0)] + [Alias("PSPath")] + [ValidateScript( { if ([IO.Path]::GetExtension($_) -ne ".psd1") { + throw "Path must point to a .psd1 file" + } $true })] + [string]$Path, + + # The property to be set in the manifest. It must already exist in the file (and not be commented out) + # This searches the Manifest root properties, then the properties PrivateData, then the PSData + [Parameter(ParameterSetName = "Overwrite")] + [string]$PropertyName = 'ModuleVersion', + + # A new value for the property + [Parameter(ParameterSetName = "Overwrite", Mandatory)] + $Value, + + # By default Update-Metadata increments ModuleVersion; this controls which part of the version number is incremented + [Parameter(ParameterSetName = "IncrementVersion")] + [ValidateSet("Major", "Minor", "Build", "Revision")] + [string]$Increment = "Build", + + # When set, and incrementing the ModuleVersion, output the new version number. + [Parameter(ParameterSetName = "IncrementVersion")] + [switch]$Passthru + ) + process { + $KeyValue = Get-Metadata $Path -PropertyName $PropertyName -Passthru + + if ($PSCmdlet.ParameterSetName -eq "IncrementVersion") { + $Version = [Version]$KeyValue.GetPureExpression().Value # SafeGetValue() + + $Version = switch ($Increment) { + "Major" { + [Version]::new($Version.Major + 1, 0) + } + "Minor" { + $Minor = if ($Version.Minor -le 0) { + 1 + } else { + $Version.Minor + 1 + } + [Version]::new($Version.Major, $Minor) + } + "Build" { + $Build = if ($Version.Build -le 0) { + 1 + } else { + $Version.Build + 1 + } + [Version]::new($Version.Major, $Version.Minor, $Build) + } + "Revision" { + $Build = if ($Version.Build -le 0) { + 0 + } else { + $Version.Build + } + $Revision = if ($Version.Revision -le 0) { + 1 + } else { + $Version.Revision + 1 + } + [Version]::new($Version.Major, $Version.Minor, $Build, $Revision) + } + } + + $Value = $Version + + if ($Passthru) { + $Value + } + } + + $Value = ConvertTo-Metadata $Value + + $Extent = $KeyValue.Extent + while ($KeyValue.parent) { + $KeyValue = $KeyValue.parent + } + + $ManifestContent = $KeyValue.Extent.Text.Remove( + $Extent.StartOffset, + ($Extent.EndOffset - $Extent.StartOffset) + ).Insert($Extent.StartOffset, $Value).Trim() + + if (Test-Path $Path) { + Set-Content -Encoding UTF8 -Path $Path -Value $ManifestContent + } else { + $ManifestContent + } + } +} +#EndRegion '.\Public\Update-Metadata.ps1' 128 +#Region '.\Public\Update-Object.ps1' 0 +function Update-Object { + <# + .Synopsis + Recursively updates a hashtable or custom object with new values + .Description + Updates the InputObject with data from the update object, updating or adding values. + .Example + Update-Object -Input @{ + One = "Un" + Two = "Dos" + } -Update @{ + One = "Uno" + Three = "Tres" + } + + Updates the InputObject with the values in the UpdateObject, + will return the following object: + + @{ + One = "Uno" + Two = "Dos" + Three = "Tres" + } + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "")] # Because PSSCriptAnalyzer team refuses to listen to reason. See bugs: #194 #283 #521 #608 + [CmdletBinding(SupportsShouldProcess)] + param( + # The object (or hashtable) with properties (or keys) to overwrite the InputObject + [AllowNull()] + [Parameter(Position = 0, Mandatory = $true)] + $UpdateObject, + + # This base object (or hashtable) will be updated and overwritten by the UpdateObject + [Parameter(ValueFromPipeline = $true, Mandatory = $true)] + $InputObject, + + # A list of values which (if found on InputObject) should not be updated from UpdateObject + [Parameter()] + [string[]]$ImportantInputProperties + ) + process { + # Write-Debug "INPUT OBJECT:" + # Write-Debug (($InputObject | Out-String -Stream | ForEach-Object TrimEnd) -join "`n") + # Write-Debug "Update OBJECT:" + # Write-Debug (($UpdateObject | Out-String -Stream | ForEach-Object TrimEnd) -join "`n") + if ($Null -eq $InputObject) { + return + } + + # $InputObject -is [PSCustomObject] -or + if ($InputObject -is [System.Collections.IDictionary]) { + $OutputObject = $InputObject + } else { + # Create a PSCustomObject with all the properties + $OutputObject = [PSObject]$InputObject # | Select-Object * | % { } + } + + if (!$UpdateObject) { + $OutputObject + return + } + + if ($UpdateObject -is [System.Collections.IDictionary]) { + $Keys = $UpdateObject.Keys + } else { + $Keys = @($UpdateObject | + Get-Member -MemberType Properties | + Where-Object { $p1 -notcontains $_.Name } | + Select-Object -ExpandProperty Name) + } + + function TestKey { + [OutputType([bool])] + [CmdletBinding()] + param($InputObject, $Key) + [bool]$( + if ($InputObject -is [System.Collections.IDictionary]) { + $InputObject.ContainsKey($Key) + } else { + Get-Member -InputObject $InputObject -Name $Key + } + ) + } + + # # Write-Debug "Keys: $Keys" + foreach ($key in $Keys) { + if ($key -notin $ImportantInputProperties -or -not (TestKey -InputObject $InputObject -Key $Key) ) { + # recurse Dictionaries (hashtables) and PSObjects + if (($OutputObject.$Key -is [System.Collections.IDictionary] -or $OutputObject.$Key -is [PSObject]) -and + ($InputObject.$Key -is [System.Collections.IDictionary] -or $InputObject.$Key -is [PSObject])) { + $Value = Update-Object -InputObject $InputObject.$Key -UpdateObject $UpdateObject.$Key + } else { + $Value = $UpdateObject.$Key + } + + if ($OutputObject -is [System.Collections.IDictionary]) { + $OutputObject.$key = $Value + } else { + $OutputObject = Add-Member -InputObject $OutputObject -MemberType NoteProperty -Name $key -Value $Value -PassThru -Force + } + } + } + + $OutputObject + } +} +#EndRegion '.\Public\Update-Object.ps1' 107 +#Region '.\Footer\InitialMetadataConverters.ps1' 0 +$MetadataSerializers = @{} +$MetadataDeserializers = @{} + +if ($Converters -is [Collections.IDictionary]) { + Add-MetadataConverter $Converters +} +function PSCredentialMetadataConverter { + <# + .Synopsis + Creates a new PSCredential with the specified properties + .Description + This is just a wrapper for the PSObject constructor with -Property $Value + It exists purely for the sake of psd1 serialization + .Parameter Value + The hashtable of properties to add to the created objects + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "EncodedPassword")] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingUserNameAndPasswordParams", "")] + param( + # The UserName for this credential + [string]$UserName, + # The Password for this credential, encoded via ConvertFrom-SecureString + [string]$EncodedPassword + ) + New-Object PSCredential $UserName, (ConvertTo-SecureString $EncodedPassword) +} + +# The OriginalMetadataSerializers +Add-MetadataConverter @{ + [bool] = { if ($_) { '$True' } else { '$False' } } + [Version] = { "'$_'" } + [PSCredential] = { 'PSCredential "{0}" "{1}"' -f $_.UserName, (ConvertFrom-SecureString $_.Password) } + [SecureString] = { "ConvertTo-SecureString {0}" -f (ConvertFrom-SecureString $_) } + [Guid] = { "Guid '$_'" } + [DateTime] = { "DateTime '{0}'" -f $InputObject.ToString('o') } + [DateTimeOffset] = { "DateTimeOffset '{0}'" -f $InputObject.ToString('o') } + [ConsoleColor] = { "ConsoleColor {0}" -f $InputObject.ToString() } + + [System.Management.Automation.SwitchParameter] = { if ($_) { '$True' } else { '$False' } } + # This GUID is here instead of as a function + # just to make sure the tests can validate the converter hashtables + "Guid" = { [Guid]$Args[0] } + "DateTime" = { [DateTime]$Args[0] } + "DateTimeOffset" = { [DateTimeOffset]$Args[0] } + "ConsoleColor" = { [ConsoleColor]$Args[0] } + "ScriptBlock" = { [scriptblock]::Create($Args[0]) } + "PSCredential" = (Get-Command PSCredentialMetadataConverter).ScriptBlock + "FromPsMetadata" = { + $TypeName, $Args = $Args + # Can't construct a PowerLine.Cap with ([Type]$TypeName)::new() + $Output = New-Object $TypeName + $Output.FromPsMetadata($Args) + $Output + } + "PSObject" = { param([hashtable]$Properties, [string[]]$TypeName) + $Result = New-Object System.Management.Automation.PSObject -Property $Properties + $TypeName += @($Result.PSTypeNames) + $Result.PSTypeNames.Clear() + foreach ($Name in $TypeName) { + $Result.PSTypeNames.Add($Name) + } + $Result } + +} + +$Script:OriginalMetadataSerializers = $script:MetadataSerializers.Clone() +$Script:OriginalMetadataDeserializers = $script:MetadataDeserializers.Clone() + +Export-ModuleMember -Function *-* -Alias * +#EndRegion '.\Footer\InitialMetadataConverters.ps1' 70 diff --git a/Tools/Metadata/1.5.7/PSGetModuleInfo.xml b/Tools/Metadata/1.5.7/PSGetModuleInfo.xml new file mode 100644 index 000000000000..9adb1fa26d89 --- /dev/null +++ b/Tools/Metadata/1.5.7/PSGetModuleInfo.xml @@ -0,0 +1,154 @@ + + + + Microsoft.PowerShell.Commands.PSRepositoryItemInfo + System.Management.Automation.PSCustomObject + System.Object + + + Metadata + 1.5.7 + Module + A module for PowerShell data serialization + Joel Bennett + Jaykul + Copyright (c) 2014-2021 by Joel Bennett, all rights reserved. +
2022-08-17T05:03:00+08:00
+ +
2025-12-08T22:53:40.2167412+08:00
+ + + + Microsoft.PowerShell.Commands.DisplayHintType + System.Enum + System.ValueType + System.Object + + DateTime + 2 + + +
+ + http://opensource.org/licenses/MIT + https://github.com/PoshCode/Metadata + + + + System.Object[] + System.Array + System.Object + + + Serialization + Metadata + Development + Configuration + Settings + PSModule + + + + + System.Collections.Hashtable + System.Object + + + + Workflow + + + + + + + Cmdlet + + + + Command + + + + Add-MetadataConverter + ConvertFrom-Metadata + ConvertTo-Metadata + Export-Metadata + Get-Metadata + Import-Metadata + Test-PSVersion + Update-Metadata + Update-Object + + + + + Function + + + + Add-MetadataConverter + ConvertFrom-Metadata + ConvertTo-Metadata + Export-Metadata + Get-Metadata + Import-Metadata + Test-PSVersion + Update-Metadata + Update-Object + + + + + DscResource + + + + RoleCapability + + + + + + Fixes PSObject serialization bug + + + + + https://www.powershellgallery.com/api/v2 + PSGallery + NuGet + + + System.Management.Automation.PSCustomObject + System.Object + + + Copyright (c) 2014-2021 by Joel Bennett, all rights reserved. + A module for PowerShell data serialization + False + Fixes PSObject serialization bug + True + True + 697777 + 780046 + 15204 + 17/08/2022 5:03:00 AM +08:00 + 17/08/2022 5:03:00 AM +08:00 + 8/12/2025 2:50:00 PM +08:00 + Serialization Metadata Development Configuration Settings PSModule PSFunction_Add-MetadataConverter PSCommand_Add-MetadataConverter PSFunction_ConvertFrom-Metadata PSCommand_ConvertFrom-Metadata PSFunction_ConvertTo-Metadata PSCommand_ConvertTo-Metadata PSFunction_Export-Metadata PSCommand_Export-Metadata PSFunction_Get-Metadata PSCommand_Get-Metadata PSFunction_Import-Metadata PSCommand_Import-Metadata PSFunction_Test-PSVersion PSCommand_Test-PSVersion PSFunction_Update-Metadata PSCommand_Update-Metadata PSFunction_Update-Object PSCommand_Update-Object PSIncludes_Function + False + 2025-12-08T14:50:00Z + 1.5.7 + Joel Bennett + false + Module + Metadata.nuspec|Metadata.psd1|Metadata.psm1 + c7505d40-646d-46b5-a440-8a81791c5d23 + HuddledMasses.org + + + C:\Users\Zac\Documents\PowerShell\Modules\Metadata\1.5.7 +
+
+
diff --git a/Tools/ModuleBuilder/3.1.8/ModuleBuilder.psd1 b/Tools/ModuleBuilder/3.1.8/ModuleBuilder.psd1 new file mode 100644 index 000000000000..91c3200bd7bd --- /dev/null +++ b/Tools/ModuleBuilder/3.1.8/ModuleBuilder.psd1 @@ -0,0 +1,54 @@ +@{ + # The module version should be SemVer.org compatible + ModuleVersion = '3.1.8' + + # PrivateData is where all third-party metadata goes + PrivateData = @{ + # PrivateData.PSData is the PowerShell Gallery data + PSData = @{ + # Prerelease string should be here, so we can set it + Prerelease = '' + + # Release Notes have to be here, so we can update them + ReleaseNotes = ' + ModuleBuilder v3.1.8+Build.local.Branch.main.Sha.b4d5aaf9df98194aa7d40c46cd3d7ca787011c54.Date.20250412T215606 + Fix case sensitivity of defaults for SourceDirectories and PublicFilter + ' + + # Tags applied to this module. These help with module discovery in online galleries. + Tags = 'Authoring','Build','Development','BestPractices' + + # A URL to the license for this module. + LicenseUri = 'https://github.com/PoshCode/ModuleBuilder/blob/master/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/PoshCode/ModuleBuilder' + + # A URL to an icon representing this module. + IconUri = 'https://github.com/PoshCode/ModuleBuilder/blob/resources/ModuleBuilder.png?raw=true' + } # End of PSData + } # End of PrivateData + + # The main script module that is automatically loaded as part of this module + RootModule = 'ModuleBuilder.psm1' + + # Modules that must be imported into the global environment prior to importing this module + RequiredModules = @('Configuration') + + # Always define FunctionsToExport as an empty @() which will be replaced on build + FunctionsToExport = @('Build-Module','Convert-Breakpoint','Convert-CodeCoverage','ConvertFrom-SourceLineNumber','ConvertTo-SourceLineNumber') + AliasesToExport = @('build','Convert-LineNumber') + + # ID used to uniquely identify this module + GUID = '4775ad56-8f64-432f-8da7-87ddf7a34653' + Description = 'A module for authoring and building PowerShell modules' + + # Common stuff for all our modules: + CompanyName = 'PoshCode' + Author = 'Joel Bennett' + Copyright = "Copyright 2018 Joel Bennett" + + # Minimum version of the Windows PowerShell engine required by this module + PowerShellVersion = '5.1' + CompatiblePSEditions = @('Core','Desktop') +} diff --git a/Tools/ModuleBuilder/3.1.8/ModuleBuilder.psm1 b/Tools/ModuleBuilder/3.1.8/ModuleBuilder.psm1 new file mode 100644 index 000000000000..70e6bb32d588 --- /dev/null +++ b/Tools/ModuleBuilder/3.1.8/ModuleBuilder.psm1 @@ -0,0 +1,1390 @@ +#Region './Classes/AliasVisitor.ps1' -1 + +using namespace System.Management.Automation.Language +using namespace System.Collections.Generic + +# This is used only to parse the parameters to New|Set|Remove-Alias +# NOTE: this is _part of_ the implementation of AliasVisitor, but ... +# PowerShell can't handle nested classes so I left it outside, +# but I kept it here in this file. +class AliasParameterVisitor : AstVisitor { + [string]$Parameter = $null + [string]$Command = $null + [string]$Name = $null + [string]$Value = $null + [string]$Scope = $null + + # Parameter Names + [AstVisitAction] VisitCommandParameter([CommandParameterAst]$ast) { + $this.Parameter = $ast.ParameterName + return [AstVisitAction]::Continue + } + + # Parameter Values + [AstVisitAction] VisitStringConstantExpression([StringConstantExpressionAst]$ast) { + # The FIRST command element is always the command name + if (!$this.Command) { + $this.Command = $ast.Value + return [AstVisitAction]::Continue + } else { + # Nobody should use minimal parameters like -N for -Name ... + # But if they do, our parser works anyway! + switch -Wildcard ($this.Parameter) { + "S*" { + $this.Scope = $ast.Value + } + "N*" { + $this.Name = $ast.Value + } + "Va*" { + $this.Value = $ast.Value + } + "F*" { + if ($ast.Value) { + # Force parameter was passed as named parameter with a positional parameter after it which is alias name + $this.Name = $ast.Value + } + } + default { + if (!$this.Parameter) { + # For bare arguments, the order is Name, Value: + if (!$this.Name) { + $this.Name = $ast.Value + } else { + $this.Value = $ast.Value + } + } + } + } + + $this.Parameter = $null + + # If we have enough information, stop the visit + # For -Scope global or Remove-Alias, we don't want to export these + if ($this.Name -and $this.Command -eq "Remove-Alias") { + $this.Command = "Remove-Alias" + return [AstVisitAction]::StopVisit + } elseif ($this.Name -and $this.Scope -eq "Global") { + return [AstVisitAction]::StopVisit + } + return [AstVisitAction]::Continue + } + } + + [AliasParameterVisitor] Clear() { + $this.Command = $null + $this.Parameter = $null + $this.Name = $null + $this.Value = $null + $this.Scope = $null + return $this + } +} + +# This visits everything at the top level of the script +class AliasVisitor : AstVisitor { + [HashSet[String]]$Aliases = @() + [AliasParameterVisitor]$Parameters = @{} + + # The [Alias(...)] attribute on functions matters, but we can't export aliases that are defined inside a function + [AstVisitAction] VisitFunctionDefinition([FunctionDefinitionAst]$ast) { + @($ast.Body.ParamBlock.Attributes.Where{ + $_.TypeName.Name -eq "Alias" + }.PositionalArguments.Value).ForEach{ + if ($_) { + $this.Aliases.Add($_) + } + } + + return [AstVisitAction]::SkipChildren + } + + # Top-level commands matter, but only if they're alias commands + [AstVisitAction] VisitCommand([CommandAst]$ast) { + if ($ast.CommandElements[0].Value -imatch "(New|Set|Remove)-Alias") { + $ast.Visit($this.Parameters.Clear()) + + # We COULD just remove it (even if we didn't add it) ... + if ($this.Parameters.Command -ieq "Remove-Alias") { + # But Write-Verbose for logging purposes + if ($this.Aliases.Contains($this.Parameters.Name)) { + Write-Verbose -Message "Alias '$($this.Parameters.Name)' is removed by line $($ast.Extent.StartLineNumber): $($ast.Extent.Text)" + $this.Aliases.Remove($this.Parameters.Name) + } + # We don't need to export global aliases, because they broke out already + } elseif ($this.Parameters.Name -and $this.Parameters.Scope -ine 'Global') { + $this.Aliases.Add($this.Parameters.Name) + } + } + return [AstVisitAction]::SkipChildren + } +} +#EndRegion './Classes/AliasVisitor.ps1' 120 +#Region './Private/ConvertToAst.ps1' -1 + +function ConvertToAst { + <# + .SYNOPSIS + Parses the given code and returns an object with the AST, Tokens and ParseErrors + #> + param( + # The script content, or script or module file path to parse + [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] + [Alias("Path", "PSPath", "Definition", "ScriptBlock", "Module")] + $Code + ) + process { + Write-Debug " ENTER: ConvertToAst $Code" + $ParseErrors = $null + $Tokens = $null + if ($Code | Test-Path -ErrorAction SilentlyContinue) { + Write-Debug " Parse Code as Path" + $AST = [System.Management.Automation.Language.Parser]::ParseFile(($Code | Convert-Path), [ref]$Tokens, [ref]$ParseErrors) + } elseif ($Code -is [System.Management.Automation.FunctionInfo]) { + Write-Debug " Parse Code as Function" + $String = "function $($Code.Name) { $($Code.Definition) }" + $AST = [System.Management.Automation.Language.Parser]::ParseInput($String, [ref]$Tokens, [ref]$ParseErrors) + } else { + Write-Debug " Parse Code as String" + $AST = [System.Management.Automation.Language.Parser]::ParseInput([String]$Code, [ref]$Tokens, [ref]$ParseErrors) + } + + Write-Debug " EXIT: ConvertToAst" + [PSCustomObject]@{ + PSTypeName = "PoshCode.ModuleBuilder.ParseResults" + ParseErrors = $ParseErrors + Tokens = $Tokens + AST = $AST + } + } +} +#EndRegion './Private/ConvertToAst.ps1' 37 +#Region './Private/CopyReadMe.ps1' -1 + +function CopyReadMe { + [CmdletBinding()] + param( + # The path to the ReadMe document to copy + [Parameter(ValueFromPipelineByPropertyName)] + [AllowNull()][AllowEmptyString()] + [string]$ReadMe, + + # The name of the module -- because the file is renamed to about_$ModuleName.help.txt + [Parameter(Mandatory,ValueFromPipelineByPropertyName)] + [Alias("Name")] + [string]$ModuleName, + + [Parameter(Mandatory,ValueFromPipelineByPropertyName)] + [string]$OutputDirectory, + + # The culture (language) to store the ReadMe as (defaults to "en") + [Parameter(ValueFromPipelineByPropertyName)] + [Globalization.CultureInfo]$Culture = $(Get-UICulture), + + # If set, overwrite the existing readme + [Switch]$Force + ) + process { + # Copy the readme file as an about_ help file + Write-Verbose "Test for ReadMe: $Pwd/$($ReadMe)" + if ($ReadMe -and (Test-Path $ReadMe -PathType Leaf)) { + # Make sure there's a language path + $LanguagePath = Join-Path $OutputDirectory $Culture + if (!(Test-Path $LanguagePath -PathType Container)) { + $null = New-Item $LanguagePath -Type Directory -Force + } + Write-Verbose "Copy ReadMe to: $LanguagePath" + + $about_module = Join-Path $LanguagePath "about_$($ModuleName).help.txt" + if (!(Test-Path $about_module)) { + Write-Verbose "Turn readme into about_module" + Copy-Item -LiteralPath $ReadMe -Destination $about_module -Force:$Force + } + } + } +} +#EndRegion './Private/CopyReadMe.ps1' 43 +#Region './Private/GetBuildInfo.ps1' -1 + +function GetBuildInfo { + [CmdletBinding()] + param( + # The path to the Build Manifest Build.psd1 + [Parameter()] + [AllowNull()] + [string]$BuildManifest, + + # Pass MyInvocation from the Build-Command so we can read parameter values + [Parameter(DontShow)] + [AllowNull()] + $BuildCommandInvocation + ) + + $BuildInfo = if ($BuildManifest -and (Test-Path $BuildManifest) -and (Split-path -Leaf $BuildManifest) -eq 'build.psd1') { + # Read the build.psd1 configuration file for default parameter values + Write-Debug "Load Build Manifest $BuildManifest" + Import-Metadata -Path $BuildManifest + } else { + @{} + } + + $CommonParameters = [System.Management.Automation.Cmdlet]::CommonParameters + + [System.Management.Automation.Cmdlet]::OptionalCommonParameters + $BuildParameters = $BuildCommandInvocation.MyCommand.Parameters + # Make we can always look things up in BoundParameters + $BoundParameters = if ($BuildCommandInvocation.BoundParameters) { + $BuildCommandInvocation.BoundParameters + } else { + @{} + } + + # Combine the defaults with parameter values + $ParameterValues = @{} + if ($BuildCommandInvocation) { + foreach ($parameter in $BuildParameters.GetEnumerator().Where({$_.Key -notin $CommonParameters})) { + Write-Debug " Parameter: $($parameter.key)" + $key = $parameter.Key + + # We want to map the parameter aliases to the parameter name: + foreach ($k in @($parameter.Value.Aliases)) { + if ($null -ne $k -and $BuildInfo.ContainsKey($k)) { + Write-Debug " ... Update BuildInfo[$key] from $k" + $BuildInfo[$key] = $BuildInfo[$k] + $null = $BuildInfo.Remove($k) + } + } + # Bound parameter values > build.psd1 values > default parameters values + if (-not $BuildInfo.ContainsKey($key) -or $BoundParameters.ContainsKey($key)) { + # Reading the current value of the $key variable returns either the bound parameter or the default + if ($null -ne ($value = Get-Variable -Name $key -ValueOnly -ErrorAction Ignore )) { + if ($value -ne ($null -as $parameter.Value.ParameterType)) { + $ParameterValues[$key] = $value + } + } + if ($BoundParameters.ContainsKey($key)) { + Write-Debug " From Parameter: $($ParameterValues[$key] -join ', ')" + } elseif ($ParameterValues[$key]) { + Write-Debug " From Default: $($ParameterValues[$key] -join ', ')" + } + } elseif ($BuildInfo[$key]) { + Write-Debug " From Manifest: $($BuildInfo[$key] -join ', ')" + } + } + } + # BuildInfo.SourcePath should point to a module manifest + if ($BuildInfo.SourcePath -and $BuildInfo.SourcePath -ne $BuildManifest) { + Write-Debug " Updating: SourcePath" + Write-Debug " To: $($BuildInfo.SourcePath)" + $ParameterValues["SourcePath"] = $BuildInfo.SourcePath + } + # If SourcePath point to build.psd1, we should clear it + if ($ParameterValues["SourcePath"] -eq $BuildManifest) { + Write-Debug " Removing: SourcePath" + $ParameterValues.Remove("SourcePath") + } + Write-Debug "Finished parsing Build Manifest $BuildManifest" + + $BuildManifestParent = if ($BuildManifest) { + Split-Path -Parent $BuildManifest + } else { + Get-Location -PSProvider FileSystem + } + + if ((-not $BuildInfo.SourcePath) -and $ParameterValues["SourcePath"] -notmatch '\.psd1') { + Write-Debug " Searching: SourcePath ($BuildManifestParent/**/*.psd1)" + # Find a module manifest (or maybe several) + $ModuleInfo = Get-ChildItem $BuildManifestParent -Recurse -Filter *.psd1 -ErrorAction SilentlyContinue | + ImportModuleManifest -ErrorAction SilentlyContinue + # If we found more than one module info, the only way we have of picking just one is if it matches a folder name + if (@($ModuleInfo).Count -gt 1) { + Write-Debug (@(@(" Found $(@($ModuleInfo).Count):") + @($ModuleInfo.Path)) -join "`n ") + # It can't be a module that needs building unless it has either: + $ModuleInfo = $ModuleInfo.Where{ + $Root = Split-Path $_.Path + @( + # - A build.psd1 next to it + Test-Path (Join-Path $Root "build.ps1") -PathType Leaf + # - A Public (or Private) folder with source scripts in it + Test-Path (Join-Path $Root "Public") -PathType Container + Test-Path (Join-Path $Root "Private") -PathType Container + ) -contains $true + } + Write-Debug (@(@(" Filtered $(@($ModuleInfo).Count):") + @($ModuleInfo.Path)) -join "`n ") + } + if (@($ModuleInfo).Count -eq 1) { + Write-Debug "Updating BuildInfo SourcePath to $($ModuleInfo.Path)" + $ParameterValues["SourcePath"] = $ModuleInfo.Path + } else { + throw "Can't determine the module manifest in $BuildManifestParent" + } + } + + $BuildInfo = $BuildInfo | Update-Object $ParameterValues + Write-Debug "Using Module Manifest $($BuildInfo.SourcePath)" + + # Make sure the SourcePath is absolute and points at an actual file + if (!(Split-Path -IsAbsolute $BuildInfo.SourcePath) -and $BuildManifestParent) { + $BuildInfo.SourcePath = Join-Path $BuildManifestParent $BuildInfo.SourcePath | Convert-Path + } else { + $BuildInfo.SourcePath = Convert-Path $BuildInfo.SourcePath + } + if (!(Test-Path $BuildInfo.SourcePath)) { + throw "Can't find module manifest at the specified SourcePath: $($BuildInfo.SourcePath)" + } + + $BuildInfo +} +#EndRegion './Private/GetBuildInfo.ps1' 129 +#Region './Private/GetCommandAlias.ps1' -1 + + +function GetCommandAlias { + <# + .SYNOPSIS + Parses one or more files for aliases and returns a list of alias names. + #> + [CmdletBinding()] + [OutputType([System.Collections.Generic.Hashset[string]])] + param( + # The AST to find aliases in + [Parameter(Mandatory, ValueFromPipelineByPropertyName, ValueFromPipeline)] + [System.Management.Automation.Language.Ast]$Ast + ) + begin { + $Visitor = [AliasVisitor]::new() + } + process { + $Ast.Visit($Visitor) + } + end { + $Visitor.Aliases + } +} + +#EndRegion './Private/GetCommandAlias.ps1' 25 +#Region './Private/GetRelativePath.ps1' -1 + +function GetRelativePath { + <# + .SYNOPSIS + Returns the relative path, or $Path if the paths don't share the same root. + For backward compatibility, this is [System.IO.Path]::GetRelativePath for .NET 4.x + #> + [OutputType([string])] + [CmdletBinding()] + param( + # The source path the result should be relative to. This path is always considered to be a directory. + [Parameter(Mandatory)] + [string]$RelativeTo, + + # The destination path. + [Parameter(Mandatory)] + [string]$Path + ) + + # This giant mess is because PowerShell drives aren't valid filesystem drives + $Drive = $Path -replace "^([^\\/]+:[\\/])?.*", '$1' + if ($Drive -ne ($RelativeTo -replace "^([^\\/]+:[\\/])?.*", '$1')) { + Write-Verbose "Paths on different drives" + return $Path # no commonality, different drive letters on windows + } + $RelativeTo = $RelativeTo -replace "^[^\\/]+:[\\/]", [IO.Path]::DirectorySeparatorChar + $Path = $Path -replace "^[^\\/]+:[\\/]", [IO.Path]::DirectorySeparatorChar + $RelativeTo = [IO.Path]::GetFullPath($RelativeTo).TrimEnd('\/') -replace "^[^\\/]+:[\\/]", [IO.Path]::DirectorySeparatorChar + $Path = [IO.Path]::GetFullPath($Path) -replace "^[^\\/]+:[\\/]", [IO.Path]::DirectorySeparatorChar + + $commonLength = 0 + while ($Path[$commonLength] -eq $RelativeTo[$commonLength]) { + $commonLength++ + } + if ($commonLength -eq $RelativeTo.Length -and $RelativeTo.Length -eq $Path.Length) { + Write-Verbose "Equal Paths" + return "." # The same paths + } + if ($commonLength -eq 0) { + Write-Verbose "Paths on different drives?" + return $Drive + $Path # no commonality, different drive letters on windows + } + + Write-Verbose "Common base: $commonLength $($RelativeTo.Substring(0,$commonLength))" + # In case we matched PART of a name, like C:\Users\Joel and C:\Users\Joe + while ($commonLength -gt $RelativeTo.Length -and ($RelativeTo[$commonLength] -ne [IO.Path]::DirectorySeparatorChar)) { + $commonLength-- + } + + Write-Verbose "Common base: $commonLength $($RelativeTo.Substring(0,$commonLength))" + # create '..' segments for segments past the common on the "$RelativeTo" path + if ($commonLength -lt $RelativeTo.Length) { + $result = @('..') * @($RelativeTo.Substring($commonLength).Split([IO.Path]::DirectorySeparatorChar).Where{ $_ }).Length -join ([IO.Path]::DirectorySeparatorChar) + } + (@($result, $Path.Substring($commonLength).TrimStart([IO.Path]::DirectorySeparatorChar)).Where{ $_ } -join ([IO.Path]::DirectorySeparatorChar)) +} +#EndRegion './Private/GetRelativePath.ps1' 56 +#Region './Private/ImportModuleManifest.ps1' -1 + +function ImportModuleManifest { + [CmdletBinding()] + param( + [Alias("PSPath")] + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] + [string]$Path + ) + process { + # Get all the information in the module manifest + $ModuleInfo = Get-Module $Path -ListAvailable -WarningAction SilentlyContinue -ErrorAction SilentlyContinue -ErrorVariable Problems + + # Some versions fails silently. If the GUID is empty, we didn't get anything at all + if ($ModuleInfo.Guid -eq [Guid]::Empty) { + Write-Error "Cannot parse '$Path' as a module manifest, try Test-ModuleManifest for details" + return + } + + # Some versions show errors are when the psm1 doesn't exist (yet), but we don't care + $ErrorsWeIgnore = "^" + (@( + "Modules_InvalidRequiredModulesinModuleManifest" + "Modules_InvalidRootModuleInModuleManifest" + ) -join "|^") + + # If there are any OTHER problems we'll fail + if ($Problems = $Problems.Where({ $_.FullyQualifiedErrorId -notmatch $ErrorsWeIgnore })) { + foreach ($problem in $Problems) { + Write-Error $problem + } + # Short circuit - don't output the ModuleInfo if there were errors + return + } + + # Workaround the fact that Get-Module returns the DefaultCommandPrefix as Prefix + Update-Object -InputObject $ModuleInfo -UpdateObject @{ DefaultCommandPrefix = $ModuleInfo.Prefix; Prefix = "" } + } +} +#EndRegion './Private/ImportModuleManifest.ps1' 37 +#Region './Private/InitializeBuild.ps1' -1 + +function InitializeBuild { + <# + .SYNOPSIS + Loads build.psd1 and the module manifest and combines them with the parameter values of the calling function. + .DESCRIPTION + This function is for internal use from Build-Module only + It does a few things that make it really only work properly there: + + 1. It calls ResolveBuildManifest to resolve the Build.psd1 from the given -SourcePath (can be Folder, Build.psd1 or Module manifest path) + 2. Then calls GetBuildInfo to read the Build configuration file and override parameters passed through $Invocation (read from the PARENT MyInvocation) + 2. It gets the Module information from the ModuleManifest, and merges it with the $ModuleInfo + .NOTES + Depends on the Configuration module Update-Object and (the built in Import-LocalizedData and Get-Module) + #> + [CmdletBinding()] + param( + # The root folder where the module source is (including the Build.psd1 and the module Manifest.psd1) + [string]$SourcePath, + + [Parameter(DontShow)] + [AllowNull()] + $BuildCommandInvocation = $(Get-Variable MyInvocation -Scope 1 -ValueOnly) + ) + Write-Debug "Initializing build variables" + + # GetBuildInfo reads the parameter values from the Build-Module command and combines them with the Manifest values + $BuildManifest = ResolveBuildManifest $SourcePath + + Write-Debug "BuildCommand: $( + @( + @($BuildCommandInvocation.MyCommand.Name) + @($BuildCommandInvocation.BoundParameters.GetEnumerator().ForEach{ "-{0} '{1}'" -f $_.Key, $_.Value }) + ) -join ' ')" + $BuildInfo = GetBuildInfo -BuildManifest $BuildManifest -BuildCommandInvocation $BuildCommandInvocation + + # Normalize the version (if it was passed in via build.psd1) + if ($BuildInfo.SemVer) { + Write-Verbose "Update the Version, Prerelease, and BuildMetadata from the SemVer (in case it was passed in via build.psd1)" + $BuildInfo = $BuildInfo | Update-Object @{ + Prerelease = $BuildInfo.SemVer.Split("+")[0].Split("-", 2)[1] + BuildMetadata = $BuildInfo.SemVer.Split("+", 2)[1] + Version = if (($V = $BuildInfo.SemVer.Split("+")[0].Split("-", 2)[0])) { + [version]$V + } + } + } elseif($BuildInfo.Version) { + Write-Verbose "Calculate the Semantic Version from the Version - Prerelease + BuildMetadata" + $SemVer = "$($BuildInfo.Version)" + if ($BuildInfo.Prerelease) { + $SemVer = "$SemVer-$($BuildInfo.Prerelease)" + } + if ($BuildInfo.BuildMetadata) { + $SemVer = "$SemVer+$($BuildInfo.BuildMetadata)" + } + $BuildInfo = $BuildInfo | Update-Object @{ SemVer = $SemVer } + } + + # Override VersionedOutputDirectory with UnversionedOutputDirectory + if ($BuildInfo.UnversionedOutputDirectory -and $BuildInfo.VersionedOutputDirectory) { + $BuildInfo.VersionedOutputDirectory = $false + } + + # Finally, add all the information in the module manifest to the return object + if ($ModuleInfo = ImportModuleManifest $BuildInfo.SourcePath) { + # Update the module manifest with our build configuration and output it + Update-Object -InputObject $ModuleInfo -UpdateObject $BuildInfo + } else { + throw "Unresolvable problems in module manifest: '$($BuildInfo.SourcePath)'" + } +} +#EndRegion './Private/InitializeBuild.ps1' 71 +#Region './Private/MoveUsingStatements.ps1' -1 + +function MoveUsingStatements { + <# + .SYNOPSIS + A command to comment out and copy to the top of the file the Using Statements + .DESCRIPTION + When all files are merged together, the Using statements from individual files + don't necessarily end up at the beginning of the PSM1, creating Parsing Errors. + + This function uses AST to comment out those statements (to preserver line numbering) + and insert them (conserving order) at the top of the script. + #> + [CmdletBinding()] + param( + # Path to the PSM1 file to amend + [Parameter(Mandatory, ValueFromPipelineByPropertyName, ValueFromPipeline)] + [System.Management.Automation.Language.Ast]$AST, + + [Parameter(ValueFromPipelineByPropertyName, ValueFromPipeline)] + [AllowNull()] + [System.Management.Automation.Language.ParseError[]]$ParseErrors, + + # The encoding defaults to UTF8 (or UTF8NoBom on Core) + [Parameter(DontShow)] + [string]$Encoding = $(if ($IsCoreCLR) { + "UTF8NoBom" + } else { + "UTF8" + }) + ) + process { + # Avoid modifying the file if there's no Parsing Error caused by Using Statements or other errors + if (!$ParseErrors.Where{ $_.ErrorId -eq 'UsingMustBeAtStartOfScript' }) { + Write-Debug "No using statement errors found." + return + } else { + # as decided https://github.com/PoshCode/ModuleBuilder/issues/96 + Write-Debug "Parsing errors found. We'll still attempt to Move using statements." + } + + # Find all Using statements including those non erroring (to conserve their order) + $UsingStatementExtents = $AST.FindAll( + { $Args[0] -is [System.Management.Automation.Language.UsingStatementAst] }, + $false + ).Extent + + # Edit the Script content by commenting out existing statements (conserving line numbering) + $ScriptText = $AST.Extent.Text + $InsertedCharOffset = 0 + $StatementsToCopy = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) + + foreach ($UsingSatement in $UsingStatementExtents) { + $ScriptText = $ScriptText.Insert($UsingSatement.StartOffset + $InsertedCharOffset, '#') + $InsertedCharOffset++ + + # Keep track of unique statements we'll need to insert at the top + $null = $StatementsToCopy.Add($UsingSatement.Text) + } + + $ScriptText = $ScriptText.Insert(0, ($StatementsToCopy -join "`r`n") + "`r`n") + $null = Set-Content -Value $ScriptText -Path $RootModule -Encoding $Encoding + + # Verify we haven't introduced new Parsing errors + $null = [System.Management.Automation.Language.Parser]::ParseFile( + $RootModule, + [ref]$null, + [ref]$ParseErrors + ) + + if ($ParseErrors.Count) { + $Message = $ParseErrors | + Format-Table -Auto @{n = "File"; expr = { $_.Extent.File | Split-Path -Leaf }}, + @{n = "Line"; expr = { $_.Extent.StartLineNumber }}, + Extent, ErrorId, Message | Out-String + Write-Warning "Parse errors in build output:`n$Message" + } + } +} +#EndRegion './Private/MoveUsingStatements.ps1' 78 +#Region './Private/ParameterValues.ps1' -1 + +Update-TypeData -TypeName System.Management.Automation.InvocationInfo -MemberName ParameterValues -MemberType ScriptProperty -Value { + $results = @{} + foreach ($key in $this.MyCommand.Parameters.Keys) { + if ($this.BoundParameters.ContainsKey($key)) { + $results.$key = $this.BoundParameters.$key + } elseif ($value = Get-Variable -Name $key -Scope 1 -ValueOnly -ErrorAction Ignore) { + $results.$key = $value + } + } + return $results +} -Force +#EndRegion './Private/ParameterValues.ps1' 12 +#Region './Private/ParseLineNumber.ps1' -1 + +function ParseLineNumber { + <# + .SYNOPSIS + Parses the SourceFile and SourceLineNumber from a position message + .DESCRIPTION + Parses messages like: + at , : line 1 + at C:\Test\Path\ErrorMaker.ps1:31 char:1 + at C:\Test\Path\Modules\ErrorMaker\ErrorMaker.psm1:27 char:4 + #> + [Cmdletbinding()] + param( + # A position message, starting with "at ..." and containing a line number + [Parameter(ValueFromPipeline)] + [string]$PositionMessage + ) + process { + foreach($line in $PositionMessage -split "\r?\n") { + # At (optional invocation,) :(maybe " line ") number + if ($line -match "at(?: (?[^,]+),)?\s+(?.+):(?\d+)(?: char:(?\d+))?") { + [PSCustomObject]@{ + PSTypeName = "Position" + SourceFile = $matches.SourceFile + SourceLineNumber = $matches.SourceLineNumber + OffsetInLine = $matches.OffsetInLine + PositionMessage = $line + PSScriptRoot = Split-Path $matches.SourceFile + PSCommandPath = $matches.SourceFile + InvocationBlock = $matches.InvocationBlock + } + } elseif($line -notmatch "\s*\+") { + Write-Warning "Can't match: '$line'" + } + } + } +} +#EndRegion './Private/ParseLineNumber.ps1' 37 +#Region './Private/ResolveBuildManifest.ps1' -1 + +function ResolveBuildManifest { + [CmdletBinding()] + param( + # The Source folder path, the Build Manifest Path, or the Module Manifest path used to resolve the Build.psd1 + [Alias("BuildManifest")] + [string]$SourcePath = $(Get-Location -PSProvider FileSystem) + ) + Write-Debug "ResolveBuildManifest $SourcePath" + if ((Split-Path $SourcePath -Leaf) -eq 'build.psd1') { + $BuildManifest = $SourcePath + } elseif (Test-Path $SourcePath -PathType Leaf) { + # When you pass the SourcePath as parameter, you must have the Build Manifest in the same folder + $BuildManifest = Join-Path (Split-Path -Parent $SourcePath) [Bb]uild.psd1 + } else { + # It's a container, assume the Build Manifest is directly under + $BuildManifest = Join-Path $SourcePath [Bb]uild.psd1 + } + + # Make sure we are resolving the absolute path to the manifest, and test it exists + $ResolvedBuildManifest = (Resolve-Path $BuildManifest -ErrorAction SilentlyContinue).Path + + if ($ResolvedBuildManifest) { + $ResolvedBuildManifest + } + +} +#EndRegion './Private/ResolveBuildManifest.ps1' 27 +#Region './Private/ResolveOutputFolder.ps1' -1 + +function ResolveOutputFolder { + [CmdletBinding()] + param( + # The name of the module to build + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] + [Alias("Name")] + [string]$ModuleName, + + # Where to resolve the $OutputDirectory from when relative + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] + [Alias("ModuleBase")] + [string]$Source, + + # Where to build the module. + # Defaults to an \output folder, adjacent to the "SourcePath" folder + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] + [string]$OutputDirectory, + + # specifies the module version for use in the output path if -VersionedOutputDirectory is true + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] + [Alias("ModuleVersion")] + [string]$Version, + + # If set (true) adds a folder named after the version number to the OutputDirectory + [Parameter(ValueFromPipelineByPropertyName)] + [Alias("Force")] + [switch]$VersionedOutputDirectory, + + # Controls whether or not there is a build or cleanup performed + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] + [ValidateSet("Clean", "Build", "CleanBuild")] + [string]$Target = "CleanBuild" + ) + process { + Write-Verbose "Resolve OutputDirectory path: $OutputDirectory" + + # Ensure the OutputDirectory makes sense (it's never blank anymore) + if (!(Split-Path -IsAbsolute $OutputDirectory)) { + # Relative paths are relative to the ModuleBase + $OutputDirectory = Join-Path $Source $OutputDirectory + } + # If they passed in a path with ModuleName\Version on the end... + if ((Split-Path $OutputDirectory -Leaf).EndsWith($Version) -and (Split-Path (Split-Path $OutputDirectory) -Leaf) -eq $ModuleName) { + # strip the version (so we can add it back) + $VersionedOutputDirectory = $true + $OutputDirectory = Split-Path $OutputDirectory + } + # Ensure the OutputDirectory is named "ModuleName" + if ((Split-Path $OutputDirectory -Leaf) -ne $ModuleName) { + # If it wasn't, add a "ModuleName" + $OutputDirectory = Join-Path $OutputDirectory $ModuleName + } + # Ensure the OutputDirectory is not a parent of the SourceDirectory + $RelativeOutputPath = GetRelativePath $OutputDirectory $Source + if (-not $RelativeOutputPath.StartsWith("..") -and $RelativeOutputPath -ne $Source) { + Write-Verbose "Added Version to OutputDirectory path: $OutputDirectory" + $OutputDirectory = Join-Path $OutputDirectory $Version + } + # Ensure the version number is on the OutputDirectory if it's supposed to be + if ($VersionedOutputDirectory -and -not (Split-Path $OutputDirectory -Leaf).EndsWith($Version)) { + Write-Verbose "Added Version to OutputDirectory path: $OutputDirectory" + $OutputDirectory = Join-Path $OutputDirectory $Version + } + + if (Test-Path $OutputDirectory -PathType Leaf) { + throw "Unable to build. There is a file in the way at $OutputDirectory" + } + + if ($Target -match "Clean") { + Write-Verbose "Cleaning $OutputDirectory" + if (Test-Path $OutputDirectory -PathType Container) { + Remove-Item $OutputDirectory -Recurse -Force + } + } + if ($Target -match "Build") { + # Make sure the OutputDirectory exists (relative to ModuleBase or absolute) + New-Item $OutputDirectory -ItemType Directory -Force | Convert-Path + } + } +} +#EndRegion './Private/ResolveOutputFolder.ps1' 81 +#Region './Private/SetModuleContent.ps1' -1 + +function SetModuleContent { + <# + .SYNOPSIS + A wrapper for Set-Content that handles arrays of file paths + .DESCRIPTION + The implementation here is strongly dependent on Build-Module doing the right thing + Build-Module can optionally pass a PREFIX or SUFFIX, but otherwise only passes files + + Because of that, SetModuleContent doesn't test for that + + The goal here is to pretend this is a pipeline, for the sake of memory and file IO + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "OutputPath", Justification = "The rule is buggy")] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "Encoding", Justification = "The rule is buggy ")] + [CmdletBinding()] + param( + # Where to write the joined output + [Parameter(Position=0, Mandatory)] + [string]$OutputPath, + + # Input files, the scripts that will be copied to the output path + # The FIRST and LAST items can be text content instead of file paths. + [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] + [Alias("PSPath", "FullName")] + [AllowEmptyCollection()] + [string[]]$SourceFile, + + # The working directory (allows relative paths for other values) + [string]$WorkingDirectory = $pwd, + + # The encoding defaults to UTF8 (or UTF8NoBom on Core) + [Parameter(DontShow)] + [string]$Encoding = $(if($IsCoreCLR) { "UTF8Bom" } else { "UTF8" }) + ) + begin { + Write-Debug "SetModuleContent WorkingDirectory $WorkingDirectory" + Push-Location $WorkingDirectory -StackName SetModuleContent + $ContentStarted = $false # There has been no content yet + + # Create a proxy command style scriptblock for Set-Content to keep the file handle open + $SetContentCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Management\Set-Content', [System.Management.Automation.CommandTypes]::Cmdlet) + $SetContent = {& $SetContentCmd -Path $OutputPath -Encoding $Encoding}.GetSteppablePipeline($myInvocation.CommandOrigin) + $SetContent.Begin($true) + } + process { + foreach($file in $SourceFile) { + if($SourceName = Resolve-Path $file -Relative -ErrorAction SilentlyContinue) { + Write-Verbose "Adding $SourceName" + # Setting offset to -1 because of the new line we're adding. + # This is needed for the code coverage calculation. + $SetContent.Process("#Region '$SourceName' -1`n") + Get-Content $SourceName -OutVariable source | ForEach-Object { $SetContent.Process($_) } + $SetContent.Process("#EndRegion '$SourceName' $($Source.Count+1)") + } else { + if(!$ContentStarted) { + $SetContent.Process("#Region 'PREFIX' -1`n") + $SetContent.Process($file) + $SetContent.Process("#EndRegion 'PREFIX'") + $ContentStarted = $true + } else { + $SetContent.Process("#Region 'SUFFIX' -1`n") + $SetContent.Process($file) + $SetContent.Process("#EndRegion 'SUFFIX'") + } + } + } + } + end { + $SetContent.End() + Pop-Location -StackName SetModuleContent + } +} +#EndRegion './Private/SetModuleContent.ps1' 73 +#Region './Public/Build-Module.ps1' -1 + +function Build-Module { + <# + .Synopsis + Compile a module from ps1 files to a single psm1 + + .Description + Compiles modules from source according to conventions: + 1. A single ModuleName.psd1 manifest file with metadata + 2. Source subfolders in the same directory as the Module manifest: + Enum, Classes, Private, Public contain ps1 files + 3. Optionally, a build.psd1 file containing settings for this function + + The optimization process: + 1. The OutputDirectory is created + 2. All psd1/psm1/ps1xml files (except build.psd1) in the Source will be copied to the output + 3. If specified, $CopyPaths (relative to the Source) will be copied to the output + 4. The ModuleName.psm1 will be generated (overwritten completely) by concatenating all .ps1 files in the $SourceDirectories subdirectories + 5. The ModuleVersion and ExportedFunctions in the ModuleName.psd1 may be updated (depending on parameters) + + .Example + Build-Module -Suffix "Export-ModuleMember -Function *-* -Variable PreferenceVariable" + + This example shows how to build a simple module from it's manifest, adding an Export-ModuleMember as a Suffix + + .Example + Build-Module -Prefix "using namespace System.Management.Automation" + + This example shows how to build a simple module from it's manifest, adding a using statement at the top as a prefix + + .Example + $gitVersion = gitversion | ConvertFrom-Json | Select -Expand InformationalVersion + Build-Module -SemVer $gitVersion + + This example shows how to use a semantic version from gitversion to version your build. + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "", Justification="Build is approved now")] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseCmdletCorrectly", "")] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "", Justification="Parameter handling is in InitializeBuild")] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidDefaultValueSwitchParameter", "", Justification = "VersionedOutputDirectory is Deprecated")] + [CmdletBinding(DefaultParameterSetName="SemanticVersion")] + [Alias("build")] + param( + # The path to the module folder, manifest or build.psd1 + [Parameter(Position = 0, ValueFromPipelineByPropertyName)] + [ValidateScript({ + if (Test-Path $_) { + $true + } else { + throw "Source must point to a valid module" + } + })] + [Alias("ModuleManifest", "Path")] + [string]$SourcePath = $(Get-Location -PSProvider FileSystem), + + # Where to build the module. Defaults to "../Output" adjacent to the "SourcePath" folder. + # The ACTUAL output may be in a subfolder of this path ending with the module name and version + # The default value is ../Output which results in the build going to ../Output/ModuleName/1.2.3 + [Alias("Destination")] + [string]$OutputDirectory = "../Output", + + # DEPRECATED. Now defaults true, producing a OutputDirectory with a version number as the last folder + [switch]$VersionedOutputDirectory = $true, + + # Overrides the VersionedOutputDirectory, producing an OutputDirectory without a version number as the last folder + [switch]$UnversionedOutputDirectory, + + # Semantic version, like 1.0.3-beta01+sha.22c35ffff166f34addc49a3b80e622b543199cc5 + # If the SemVer has metadata (after a +), then the full Semver will be added to the ReleaseNotes + [Parameter(ParameterSetName="SemanticVersion")] + [string]$SemVer, + + # The module version (must be a valid System.Version such as PowerShell supports for modules) + [Alias("ModuleVersion")] + [Parameter(ParameterSetName="ModuleVersion", Mandatory)] + [version]$Version = $(if(($V = $SemVer.Split("+")[0].Split("-",2)[0])){$V}), + + # Setting pre-release forces the release to be a pre-release. + # Must be valid pre-release tag like PowerShellGet supports + [Parameter(ParameterSetName="ModuleVersion")] + [string]$Prerelease = $($SemVer.Split("+")[0].Split("-",2)[1]), + + # Build metadata (like the commit sha or the date). + # If a value is provided here, then the full Semantic version will be inserted to the release notes: + # Like: ModuleName v(Version(-Prerelease?)+BuildMetadata) + [Parameter(ParameterSetName="ModuleVersion")] + [string]$BuildMetadata = $($SemVer.Split("+",2)[1]), + + # Folders which should be copied intact to the module output + # Can be relative to the module folder + [AllowEmptyCollection()] + [Alias("CopyDirectories")] + [string[]]$CopyPaths = @(), + + # Folders which contain source .ps1 scripts to be concatenated into the module + # Defaults to Enum, Classes, Private, Public + [string[]]$SourceDirectories = @( + "[Ee]num", "[Cc]lasses", "[Pp]rivate", "[Pp]ublic" + ), + + # A Filter (relative to the module folder) for public functions + # If non-empty, FunctionsToExport will be set with the file BaseNames of matching files + # Defaults to Public/*.ps1 + [AllowEmptyString()] + [string[]]$PublicFilter = "[Pp]ublic/*.ps1", + + # A switch that allows you to disable the update of the AliasesToExport + # By default, (if PublicFilter is not empty, and this is not set) + # Build-Module updates the module manifest FunctionsToExport and AliasesToExport + # with the combination of all the values in [Alias()] attributes on public functions + # and aliases created with `New-ALias` or `Set-Alias` at script level in the module + [Alias("IgnoreAliasAttribute")] + [switch]$IgnoreAlias, + + # File encoding for output RootModule (defaults to UTF8) + # Converted to System.Text.Encoding for PowerShell 6 (and something else for PowerShell 5) + [ValidateSet("UTF8", "UTF8Bom", "UTF8NoBom", "UTF7", "ASCII", "Unicode", "UTF32")] + [string]$Encoding = $(if($IsCoreCLR) { "UTF8Bom" } else { "UTF8" }), + + # The prefix is either the path to a file (relative to the module folder) or text to put at the top of the file. + # If the value of prefix resolves to a file, that file will be read in, otherwise, the value will be used. + # The default is nothing. See examples for more details. + [string]$Prefix, + + # The Suffix is either the path to a file (relative to the module folder) or text to put at the bottom of the file. + # If the value of Suffix resolves to a file, that file will be read in, otherwise, the value will be used. + # The default is nothing. See examples for more details. + [Alias("ExportModuleMember","Postfix")] + [string]$Suffix, + + # Controls whether we delete the output folder and whether we build the output + # There are three options: + # - Clean deletes the build output folder + # - Build builds the module output + # - CleanBuild first deletes the build output folder and then builds the module back into it + # Note that the folder to be deleted is the actual calculated output folder, with the version number + # So for the default OutputDirectory with version 1.2.3, the path to clean is: ../Output/ModuleName/1.2.3 + [ValidateSet("Clean", "Build", "CleanBuild")] + [string]$Target = "CleanBuild", + + # Output the ModuleInfo of the "built" module + [switch]$Passthru + ) + + begin { + if ($Encoding -notmatch "UTF8") { + Write-Warning "For maximum portability, we strongly recommend you build your script modules with UTF8 encoding (with a BOM, for backwards compatibility to PowerShell 5)." + } + } + process { + try { + # Push into the module source (it may be a subfolder) + $ModuleInfo = InitializeBuild $SourcePath + Write-Progress "Building $($ModuleInfo.Name)" -Status "Use -Verbose for more information" + Write-Verbose "Building $($ModuleInfo.Name)" + + # Ensure the OutputDirectory (exists for build, or is cleaned otherwise) + $OutputDirectory = $ModuleInfo | ResolveOutputFolder + if ($ModuleInfo.Target -notmatch "Build") { + return + } + $RootModule = Join-Path $OutputDirectory "$($ModuleInfo.Name).psm1" + $OutputManifest = Join-Path $OutputDirectory "$($ModuleInfo.Name).psd1" + Write-Verbose "Output to: $OutputDirectory" + + # Skip the build if it's up to date already + Write-Verbose "Target $($ModuleInfo.Target)" + $NewestBuild = (Get-Item $RootModule -ErrorAction SilentlyContinue).LastWriteTime + $IsNew = Get-ChildItem $ModuleInfo.ModuleBase -Recurse | + Where-Object LastWriteTime -gt $NewestBuild | + Select-Object -First 1 -ExpandProperty LastWriteTime + + if ($null -eq $IsNew) { + # This is mostly for testing ... + if ($ModuleInfo.Passthru) { + Get-Module $OutputManifest -ListAvailable + } + return # Skip the build + } + + # Note that the module manifest parent folder is the "root" of the source directories + Push-Location $ModuleInfo.ModuleBase -StackName Build-Module + + Write-Verbose "Copy files to $OutputDirectory" + # Copy the files and folders which won't be processed + Copy-Item *.psm1, *.psd1, *.ps1xml -Exclude "build.psd1" -Destination $OutputDirectory -Force + if ($ModuleInfo.CopyPaths) { + Write-Verbose "Copy Entire Directories: $($ModuleInfo.CopyPaths)" + Copy-Item -Path $ModuleInfo.CopyPaths -Recurse -Destination $OutputDirectory -Force + } + + Write-Verbose "Combine scripts to $RootModule" + + # SilentlyContinue because there don't *HAVE* to be functions at all + Write-Debug " SourceDirectories: $($ModuleInfo.ModuleBase) + $($ModuleInfo.SourceDirectories -join '|')" + $AllScripts = @($ModuleInfo.SourceDirectories).ForEach{ + # By explicitly converting, we support wildcards in the SourceDirectories parameter + if ($SourceDirectory = Join-Path -Path $ModuleInfo.ModuleBase -ChildPath $_ | Convert-Path -ErrorAction SilentlyContinue) { + Write-Debug " SourceDirectory: $SourceDirectory" + Get-ChildItem -Path $SourceDirectory -Filter *.ps1 -Recurse -ErrorAction SilentlyContinue | + Sort-Object -Property 'FullName' + } + } + + # We have to force the Encoding to string because PowerShell Core made up encodings + SetModuleContent -Source (@($ModuleInfo.Prefix) + $AllScripts.FullName + @($ModuleInfo.Suffix)).Where{$_} -Output $RootModule -Encoding "$($ModuleInfo.Encoding)" + + $ParseResult = ConvertToAst $RootModule + $ParseResult | MoveUsingStatements -Encoding "$($ModuleInfo.Encoding)" + + # If there is a PublicFilter, update ExportedFunctions + if ($ModuleInfo.PublicFilter) { + # SilentlyContinue because there don't *HAVE* to be public functions + if (($PublicFunctions = Get-ChildItem $ModuleInfo.PublicFilter -Recurse -ErrorAction SilentlyContinue | + Where-Object BaseName -in $AllScripts.BaseName | + Select-Object -ExpandProperty BaseName)) { + + Update-Metadata -Path $OutputManifest -PropertyName FunctionsToExport -Value $PublicFunctions + } + } + + # In order to support aliases to files, such as required by Invoke-Build, always export aliases + if (-not $ModuleInfo.IgnoreAlias) { + if (($AliasesToExport = $ParseResult | GetCommandAlias)) { + Update-Metadata -Path $OutputManifest -PropertyName AliasesToExport -Value $AliasesToExport + } + } + + try { + if ($ModuleInfo.Version) { + Write-Verbose "Update Manifest at $OutputManifest with version: $($ModuleInfo.Version)" + Update-Metadata -Path $OutputManifest -PropertyName ModuleVersion -Value $ModuleInfo.Version + } + } catch { + Write-Warning "Failed to update version to $($ModuleInfo.Version). $_" + } + + if ($null -ne (Get-Metadata -Path $OutputManifest -PropertyName PrivateData.PSData.Prerelease -ErrorAction SilentlyContinue)) { + if ($ModuleInfo.Prerelease) { + Write-Verbose "Update Manifest at $OutputManifest with Prerelease: $($ModuleInfo.Prerelease)" + Update-Metadata -Path $OutputManifest -PropertyName PrivateData.PSData.Prerelease -Value $ModuleInfo.Prerelease + } elseif ($PSCmdlet.ParameterSetName -eq "SemanticVersion" -or $PSBoundParameters.ContainsKey("Prerelease")) { + Update-Metadata -Path $OutputManifest -PropertyName PrivateData.PSData.Prerelease -Value "" + } + } elseif ($ModuleInfo.Prerelease) { + Write-Warning ("Cannot set Prerelease in module manifest. Add an empty Prerelease to your module manifest, like:`n" + + ' PrivateData = @{ PSData = @{ Prerelease = "" } }') + } + + if ($ModuleInfo.BuildMetadata) { + Write-Verbose "Update Manifest at $OutputManifest with metadata: $($ModuleInfo.BuildMetadata) from $($ModuleInfo.SemVer)" + $RelNote = Get-Metadata -Path $OutputManifest -PropertyName PrivateData.PSData.ReleaseNotes -ErrorAction SilentlyContinue + if ($null -ne $RelNote) { + $Line = "$($ModuleInfo.Name) v$($($ModuleInfo.SemVer))" + if ([string]::IsNullOrWhiteSpace($RelNote)) { + Write-Verbose "New ReleaseNotes:`n$Line" + Update-Metadata -Path $OutputManifest -PropertyName PrivateData.PSData.ReleaseNotes -Value $Line + } elseif ($RelNote -match "^\s*\n") { + # Leading whitespace includes newlines + Write-Verbose "Existing ReleaseNotes:$RelNote" + $RelNote = $RelNote -replace "^(?s)(\s*)\S.*$|^$","`${1}$($Line)`$_" + Write-Verbose "New ReleaseNotes:$RelNote" + Update-Metadata -Path $OutputManifest -PropertyName PrivateData.PSData.ReleaseNotes -Value $RelNote + } else { + Write-Verbose "Existing ReleaseNotes:`n$RelNote" + $RelNote = $RelNote -replace "^(?s)(\s*)\S.*$|^$","`${1}$($Line)`n`$_" + Write-Verbose "New ReleaseNotes:`n$RelNote" + Update-Metadata -Path $OutputManifest -PropertyName PrivateData.PSData.ReleaseNotes -Value $RelNote + } + } + } + + # This is mostly for testing ... + if ($ModuleInfo.Passthru) { + Get-Module $OutputManifest -ListAvailable + } + } finally { + Pop-Location -StackName Build-Module -ErrorAction SilentlyContinue + } + Write-Progress "Building $($ModuleInfo.Name)" -Completed + } +} +#EndRegion './Public/Build-Module.ps1' 282 +#Region './Public/Convert-Breakpoint.ps1' -1 + +function Convert-Breakpoint { + <# + .SYNOPSIS + Convert any breakpoints on source files to module files and vice-versa + #> + [CmdletBinding(DefaultParameterSetName="All")] + param( + [Parameter(ParameterSetName="Module")] + [switch]$ModuleOnly, + [Parameter(ParameterSetName="Source")] + [switch]$SourceOnly + ) + + if (!$SourceOnly) { + foreach ($ModuleBreakPoint in Get-PSBreakpoint | ConvertFrom-SourceLineNumber) { + Set-PSBreakpoint -Script $ModuleBreakPoint.Script -Line $ModuleBreakPoint.Line + if ($ModuleOnly) { + # TODO: | Remove-PSBreakpoint + } + } + } + + if (!$ModuleOnly) { + foreach ($SourceBreakPoint in Get-PSBreakpoint | ConvertTo-SourceLineNumber) { + if (!(Test-Path $SourceBreakPoint.SourceFile)) { + Write-Warning "Can't find source path: $($SourceBreakPoint.SourceFile)" + } else { + Set-PSBreakpoint -Script $SourceBreakPoint.SourceFile -Line $SourceBreakPoint.SourceLineNumber + } + if ($SourceOnly) { + # TODO: | Remove-PSBreakpoint + } + } + } +} +#EndRegion './Public/Convert-Breakpoint.ps1' 36 +#Region './Public/Convert-CodeCoverage.ps1' -1 + +function Convert-CodeCoverage { + <# + .SYNOPSIS + Convert the file name and line numbers from Pester code coverage of "optimized" modules to the source + .DESCRIPTION + Converts the code coverage line numbers from Pester to the source file paths. + The returned file name is always the relative path stored in the module. + .EXAMPLE + Invoke-Pester .\Tests -CodeCoverage (Get-ChildItem .\Output -Filter *.psm1).FullName -PassThru | + Convert-CodeCoverage -SourceRoot .\Source -Relative + + Runs pester tests from a "Tests" subfolder against an optimized module in the "Output" folder, + piping the results through Convert-CodeCoverage to render the code coverage misses with the source paths. + #> + param( + # The root of the source folder (for resolving source code paths) + [Parameter(Mandatory)] + [string]$SourceRoot, + + # The output of `Invoke-Pester -Pasthru` + # Note: Pester doesn't apply a custom type name + [Parameter(ValueFromPipeline)] + [PSObject]$InputObject + ) + process { + Push-Location $SourceRoot + try { + $InputObject.CodeCoverage.MissedCommands | ConvertTo-SourceLineNumber -Passthru | + Select-Object SourceFile, @{Name="Line"; Expr={$_.SourceLineNumber}}, Command + } finally { + Pop-Location + } + } +} +#EndRegion './Public/Convert-CodeCoverage.ps1' 35 +#Region './Public/ConvertFrom-SourceLineNumber.ps1' -1 + +function ConvertFrom-SourceLineNumber { + <# + .SYNOPSIS + Convert a source file path and line number to the line number in the built output + .EXAMPLE + ConvertFrom-SourceLineNumber -Module ~\2.0.0\ModuleBuilder.psm1 -SourceFile ~\Source\Public\Build-Module.ps1 -Line 27 + #> + [CmdletBinding(DefaultParameterSetName="FromString")] + param( + # The SourceFile is the source script file that was built into the module + [Parameter(Mandatory, ValueFromPipelineByPropertyName, Position=0)] + [Alias("PSCommandPath", "File", "ScriptName", "Script")] + [string]$SourceFile, + + # The SourceLineNumber (from an InvocationInfo) is the line number in the source file + [Parameter(Mandatory, ValueFromPipelineByPropertyName, Position=1)] + [Alias("LineNumber", "Line", "ScriptLineNumber")] + [int]$SourceLineNumber, + + # The name of the module in memory, or the full path to the module psm1 + [Parameter()] + [string]$Module + ) + begin { + $filemap = @{} + } + process { + if (!$Module) { + $Command = [IO.Path]::GetFileNameWithoutExtension($SourceFile) + $Module = (Get-Command $Command -ErrorAction SilentlyContinue).Source + if (!$Module) { + Write-Warning "Please specify -Module for ${SourceFile}: $SourceLineNumber" + return + } + } + if ($Module -and -not (Test-Path $Module)) { + $Module = (Get-Module $Module -ErrorAction Stop).Path + } + # Push-Location (Split-Path $SourceFile) + try { + if (!$filemap.ContainsKey($Module)) { + # Note: the new pattern is #Region but the old one was # BEGIN + $regions = Select-String '^(?:#Region|# BEGIN) (?.*) (?-?\d+)?$' -Path $Module + $filemap[$Module] = @($regions.ForEach{ + [PSCustomObject]@{ + PSTypeName = "BuildSourceMapping" + SourceFile = $_.Matches[0].Groups["SourceFile"].Value.Trim("'") + StartLineNumber = $_.LineNumber + # This offset is subtracted when calculating the line number + # because of the new line we're adding prior to the content + # of each script file in the built module. + Offset = $_.Matches[0].Groups["LineNumber"].Value + } + }) + } + + $hit = $filemap[$Module] + + if ($Source = $hit.Where{ $SourceFile.EndsWith($_.SourceFile.TrimStart(".\")) }) { + [PSCustomObject]@{ + PSTypeName = "OutputLocation" + Script = $Module + Line = $Source.StartLineNumber + $SourceLineNumber - $Source.Offset + } + } elseif($Source -eq $Module) { + [PSCustomObject]@{ + PSTypeName = "OutputLocation" + Script = $Module + Line = $SourceLineNumber - $Source.Offset + } + } else { + Write-Warning "'$SourceFile' not found in $Module" + } + } finally { + Pop-Location + } + } +} +#EndRegion './Public/ConvertFrom-SourceLineNumber.ps1' 79 +#Region './Public/ConvertTo-SourceLineNumber.ps1' -1 + +function ConvertTo-SourceLineNumber { + <# + .SYNOPSIS + Convert the line number in a built module to a file and line number in source + .EXAMPLE + ConvertTo-SourceLineNumber -SourceFile ~\ErrorMaker.psm1 -SourceLineNumber 27 + .EXAMPLE + ConvertTo-SourceLineNumber -PositionMessage "At C:\Users\Joel\OneDrive\Documents\PowerShell\Modules\ErrorMaker\ErrorMaker.psm1:27 char:4" + #> + [Alias("Convert-LineNumber")] + [CmdletBinding(DefaultParameterSetName="FromString")] + param( + # A position message as found in PowerShell's error messages, ScriptStackTrace, or InvocationInfo + [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName="FromString")] + [string]$PositionMessage, + + # The SourceFile (from an InvocationInfo) is the module psm1 path + [Parameter(Mandatory, ValueFromPipelineByPropertyName, Position=0, ParameterSetName="FromInvocationInfo")] + [Alias("PSCommandPath", "File", "ScriptName", "Script")] + [string]$SourceFile, + + # The SourceLineNumber (from an InvocationInfo) is the module line number + [Parameter(Mandatory, ValueFromPipelineByPropertyName, Position=1, ParameterSetName="FromInvocationInfo")] + [Alias("LineNumber", "Line", "ScriptLineNumber")] + [int]$SourceLineNumber, + + # The actual InvocationInfo + [Parameter(ValueFromPipeline, DontShow, ParameterSetName="FromInvocationInfo")] + [psobject]$InputObject, + + # If set, passes through the InputObject, overwriting the SourceFile and SourceLineNumber. + # Otherwise, creates a new SourceLocation object with just those properties. + [Parameter(ParameterSetName="FromInvocationInfo")] + [switch]$Passthru + ) + begin { + $filemap = @{} + } + process { + if ($PSCmdlet.ParameterSetName -eq "FromString") { + $Invocation = ParseLineNumber $PositionMessage + $SourceFile = $Invocation.SourceFile + $SourceLineNumber = $Invocation.SourceLineNumber + } + if (!(Test-Path $SourceFile)) { + throw "'$SourceFile' does not exist" + } + Push-Location (Split-Path $SourceFile) + try { + if (!$filemap.ContainsKey($SourceFile)) { + # Note: the new pattern is #Region but the old one was # BEGIN + $regions = Select-String '^(?:#Region|# BEGIN) (?.*) (?-?\d+)?$' -Path $SourceFile + if ($regions.Count -eq 0) { + Write-Warning "No SourceMap for $SourceFile" + return + } + $filemap[$SourceFile] = @($regions.ForEach{ + [PSCustomObject]@{ + PSTypeName = "BuildSourceMapping" + SourceFile = $_.Matches[0].Groups["SourceFile"].Value.Trim("'") + StartLineNumber = [System.Int32] $_.LineNumber + # This offset is added when calculating the line number + # because of the new line we're adding prior to the content + # of each script file in the built module. + Offset = $_.Matches[0].Groups["LineNumber"].Value + } + }) + } + + $hit = $filemap[$SourceFile] + + # These are all negative, because BinarySearch returns the match *after* the line we're searching for + # We need the match *before* the line we're searching for + # And we need it as a zero-based index: + $index = -2 - [Array]::BinarySearch($hit.StartLineNumber, $SourceLineNumber) + $Source = $hit[$index] + + if($Passthru) { + $InputObject | + Add-Member -MemberType NoteProperty -Name SourceFile -Value $Source.SourceFile -PassThru -Force | + Add-Member -MemberType NoteProperty -Name SourceLineNumber -Value ($SourceLineNumber - $Source.StartLineNumber + $Source.Offset) -PassThru -Force + } else { + [PSCustomObject]@{ + PSTypeName = "SourceLocation" + SourceFile = $Source.SourceFile + SourceLineNumber = $SourceLineNumber - $Source.StartLineNumber + $Source.Offset + } + } + } finally { + Pop-Location + } + } +} +#EndRegion './Public/ConvertTo-SourceLineNumber.ps1' 94 diff --git a/Tools/ModuleBuilder/3.1.8/PSGetModuleInfo.xml b/Tools/ModuleBuilder/3.1.8/PSGetModuleInfo.xml new file mode 100644 index 000000000000..f5cec11b3b14 --- /dev/null +++ b/Tools/ModuleBuilder/3.1.8/PSGetModuleInfo.xml @@ -0,0 +1,165 @@ + + + + Microsoft.PowerShell.Commands.PSRepositoryItemInfo + System.Management.Automation.PSCustomObject + System.Object + + + ModuleBuilder + 3.1.8 + Module + A module for authoring and building PowerShell modules + Joel Bennett + Jaykul + Copyright 2018 Joel Bennett +
2025-04-12T21:57:27+08:00
+ +
2025-12-08T22:53:42.0930546+08:00
+ + + + Microsoft.PowerShell.Commands.DisplayHintType + System.Enum + System.ValueType + System.Object + + DateTime + 2 + + +
+ + https://github.com/PoshCode/ModuleBuilder/blob/master/LICENSE + https://github.com/PoshCode/ModuleBuilder + https://github.com/PoshCode/ModuleBuilder/blob/resources/ModuleBuilder.png?raw=true + + + System.Object[] + System.Array + System.Object + + + Authoring + Build + Development + BestPractices + PSModule + PSEdition_Core + PSEdition_Desktop + + + + + System.Collections.Hashtable + System.Object + + + + Workflow + + + + + + + Cmdlet + + + + Command + + + + Build-Module + Convert-Breakpoint + Convert-CodeCoverage + ConvertFrom-SourceLineNumber + ConvertTo-SourceLineNumber + + + + + Function + + + + Build-Module + Convert-Breakpoint + Convert-CodeCoverage + ConvertFrom-SourceLineNumber + ConvertTo-SourceLineNumber + + + + + DscResource + + + + RoleCapability + + + + + + ModuleBuilder v3.1.8+Build.local.Branch.main.Sha.b4d5aaf9df98194aa7d40c46cd3d7ca787011c54.Date.20250412T215606_x000A_ Fix case sensitivity of defaults for SourceDirectories and PublicFilter + + + + + + System.Collections.Specialized.OrderedDictionary + System.Object + + + + Name + Configuration + + + CanonicalId + nuget:Configuration + + + + + + https://www.powershellgallery.com/api/v2 + PSGallery + NuGet + + + System.Management.Automation.PSCustomObject + System.Object + + + Copyright 2018 Joel Bennett + A module for authoring and building PowerShell modules + False + ModuleBuilder v3.1.8+Build.local.Branch.main.Sha.b4d5aaf9df98194aa7d40c46cd3d7ca787011c54.Date.20250412T215606_x000A_ Fix case sensitivity of defaults for SourceDirectories and PublicFilter + True + False + 26117 + 273822 + 20183 + 12/04/2025 9:57:27 PM +08:00 + 12/04/2025 9:57:27 PM +08:00 + 8/12/2025 2:50:00 PM +08:00 + Authoring Build Development BestPractices PSModule PSEdition_Core PSEdition_Desktop PSFunction_Build-Module PSCommand_Build-Module PSFunction_Convert-Breakpoint PSCommand_Convert-Breakpoint PSFunction_Convert-CodeCoverage PSCommand_Convert-CodeCoverage PSFunction_ConvertFrom-SourceLineNumber PSCommand_ConvertFrom-SourceLineNumber PSFunction_ConvertTo-SourceLineNumber PSCommand_ConvertTo-SourceLineNumber PSIncludes_Function + False + 2025-12-08T14:50:00Z + 3.1.8 + Joel Bennett + false + Module + ModuleBuilder.nuspec|ModuleBuilder.psd1|ModuleBuilder.psm1|en-US\about_ModuleBuilder.help.txt + 4775ad56-8f64-432f-8da7-87ddf7a34653 + 5.1 + PoshCode + + + C:\Users\Zac\Documents\PowerShell\Modules\ModuleBuilder\3.1.8 +
+
+
diff --git a/Tools/ModuleBuilder/3.1.8/en-US/about_ModuleBuilder.help.txt b/Tools/ModuleBuilder/3.1.8/en-US/about_ModuleBuilder.help.txt new file mode 100644 index 000000000000..7c7f588c86cd --- /dev/null +++ b/Tools/ModuleBuilder/3.1.8/en-US/about_ModuleBuilder.help.txt @@ -0,0 +1,88 @@ +TOPIC + about_ModuleBuilder + +SHORT DESCRIPTION + Common set of tools and patterns for module authoring by the community. + +LONG DESCRIPTION + This project is an attempt by a group of PowerShell MVPs and module authors to: + + - Build a common set of tools for module authoring + - Encourage a common pattern for organizing PowerShell module projects + - Promote best practices for authoring functions and modules + + To get started, make sure your module repository looks like the conventions described below, and + build it using the `Build-Module` command. It will create a versioned folder with an updated Module Manifest and + the PSM1 file. + + The module is opinionated and expects a few conventions to be respected so that it can: + - compile the module into a single PSM1 for improved performance + - execute Pester tests on the built artifact + - Report correct code coverage against the source's file and line numbers + (as it was before merging into the single PSM1) + - bootstrap your repository with required dependencies + + The conventions the module expects and recommends are: + 1. Create a "Source" folder in your repository with a "build.psd1" file and your module manifest in it + 2. In the "build.psd1", specify the relative Path to your module's manifest, e.g. `@{ Path = "ModuleBuilder.psd1"}` + 3. In your manifest, make sure the "FunctionsToExport" entry is not commented out. You can leave empty. + 5. Within your Source Folder, create the "Private" and "Public" folders for your functions + For each function of your module, create a file for it. The functions in "Public" will be exported from the module, + without the extention, so it's important to respect the Verb-Noun format for the name of the files. + + Here is an example from the ModuleBuilder repository. + + ModuleBuilder + ├───Source + │ │ build.psd1 + │ │ ModuleBuilder.psd1 + │ │ + │ ├───en-US + │ │ about_ModuleBuilder.help.txt + │ ├───Private + │ │ CopyHelp.ps1 + │ │ CopyReadme.ps1 + │ │ InitializeBuild.ps1 + │ │ ParameterValues.ps1 + │ │ ParseLineNumber.ps1 + │ │ ResolveModuleManifest.ps1 + │ │ ResolveModuleSource.ps1 + │ │ ResolveOutputFolder.ps1 + │ │ SetModuleContent.ps1 + │ └───Public + │ Build-Module.ps1 + │ Convert-CodeCoverage.ps1 + │ Convert-LineNumber.ps1 + └───Tests + ├───Private + │ [...] + └───Public + [...] + +EXAMPLES + PS C:\> Build-Module -SourcePath .\ModuleBuilder\Source\build.psd1 + + This will create a versioned folder of the module with ModuleBuilder.psm1 containing all functions + from the Private and Public folder, an updated ModuleBuilder.psd1 module manifest with the FunctionsToExport + correctly populated with all functions from the Public Folder. + + ModuleBuilder + └─── 1.0.0 + │ ModuleBuilder.psd1 + │ ModuleBuilder.psm1 + │ + └───en-US + about_ModuleBuilder.help.txt + + +NOTE: + Thank you to all those who contributed to this module, by writing code, sharing opinions, and provided feedback. + +TROUBLESHOOTING NOTE: + Look out on the Github repository for issues and new releases. + +SEE ALSO + - https://github.com/PoshCode/ModuleBuilder + +KEYWORDS + Module, Build, Task, Template From a52f002d5601c521998d84ca65544d561154e48e Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 8 Jun 2026 11:32:50 +0200 Subject: [PATCH 168/202] 10.5.0 version up --- version_latest.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version_latest.txt b/version_latest.txt index aa725fbb1929..2cf514e360ca 100644 --- a/version_latest.txt +++ b/version_latest.txt @@ -1 +1 @@ -10.4.5 +10.5.0 From b36a32b0a89397e211cd681ac62669efb79c79c5 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 9 Jun 2026 00:04:52 +0800 Subject: [PATCH 169/202] shuffle file locations --- .../CIPP => CIPPCore/Public}/MCP/Get-CippMcpSpec.ps1 | 0 .../CIPP => CIPPCore/Public}/MCP/Get-CippMcpToolList.ps1 | 0 .../CIPP => CIPPCore/Public}/MCP/Get-CippMcpToolResult.ps1 | 0 .../Public}/Invoke-ListObjectHistory.ps1 | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename Modules/{CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP => CIPPCore/Public}/MCP/Get-CippMcpSpec.ps1 (100%) rename Modules/{CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP => CIPPCore/Public}/MCP/Get-CippMcpToolList.ps1 (100%) rename Modules/{CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP => CIPPCore/Public}/MCP/Get-CippMcpToolResult.ps1 (100%) rename Modules/{CIPPCore/Public/Entrypoints/HTTP Functions => CIPPHTTP/Public}/Invoke-ListObjectHistory.ps1 (100%) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/MCP/Get-CippMcpSpec.ps1 b/Modules/CIPPCore/Public/MCP/Get-CippMcpSpec.ps1 similarity index 100% rename from Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/MCP/Get-CippMcpSpec.ps1 rename to Modules/CIPPCore/Public/MCP/Get-CippMcpSpec.ps1 diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/MCP/Get-CippMcpToolList.ps1 b/Modules/CIPPCore/Public/MCP/Get-CippMcpToolList.ps1 similarity index 100% rename from Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/MCP/Get-CippMcpToolList.ps1 rename to Modules/CIPPCore/Public/MCP/Get-CippMcpToolList.ps1 diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/MCP/Get-CippMcpToolResult.ps1 b/Modules/CIPPCore/Public/MCP/Get-CippMcpToolResult.ps1 similarity index 100% rename from Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/MCP/Get-CippMcpToolResult.ps1 rename to Modules/CIPPCore/Public/MCP/Get-CippMcpToolResult.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListObjectHistory.ps1 b/Modules/CIPPHTTP/Public/Invoke-ListObjectHistory.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListObjectHistory.ps1 rename to Modules/CIPPHTTP/Public/Invoke-ListObjectHistory.ps1 From fc76e111b828b514e5abdd5fbe43a2fa90740468 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 9 Jun 2026 01:03:56 +0800 Subject: [PATCH 170/202] sso app repair and fix migration failures --- .../Authentication/Add-CIPPSSOAppSecret.ps1 | 58 ++ .../Public/Authentication/New-CIPPSSOApp.ps1 | 42 +- .../Set-CIPPSSOStoredCredentials.ps1 | 54 ++ .../CIPP/Setup/Invoke-ExecSSOSetup.ps1 | 535 ++++++++++-------- 4 files changed, 426 insertions(+), 263 deletions(-) create mode 100644 Modules/CIPPCore/Public/Authentication/Add-CIPPSSOAppSecret.ps1 create mode 100644 Modules/CIPPCore/Public/Authentication/Set-CIPPSSOStoredCredentials.ps1 diff --git a/Modules/CIPPCore/Public/Authentication/Add-CIPPSSOAppSecret.ps1 b/Modules/CIPPCore/Public/Authentication/Add-CIPPSSOAppSecret.ps1 new file mode 100644 index 000000000000..7d40ec88282f --- /dev/null +++ b/Modules/CIPPCore/Public/Authentication/Add-CIPPSSOAppSecret.ps1 @@ -0,0 +1,58 @@ +function Add-CIPPSSOAppSecret { + <# + .SYNOPSIS + Creates a client secret on the CIPP-SSO app registration with retry. + .DESCRIPTION + Adds a new password credential to the given app object via Graph. Retries up to + MaxRetries times with backoff because Entra propagation can take a few seconds + after the app is freshly created or its app-management-policy exemption is set. + Throws on final failure so callers can persist Status=error + LastError. + .PARAMETER ObjectId + Graph object ID of the application (NOT the appId/clientId). + .PARAMETER DisplayName + Display name to set on the password credential. Defaults to 'CIPP-SSO-Secret'. + .PARAMETER MaxRetries + Number of secret-creation attempts before giving up. Defaults to 5. + #> + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$ObjectId, + + [Parameter(Mandatory = $false)] + [string]$DisplayName = 'CIPP-SSO-Secret', + + [Parameter(Mandatory = $false)] + [int]$MaxRetries = 5 + ) + + $SecretText = $null + $SecretAttempt = 0 + $BackoffSchedule = @(2, 5, 10, 15, 30) + $LastException = $null + + while ($SecretAttempt -lt $MaxRetries -and -not $SecretText) { + try { + $PasswordBody = @{ passwordCredential = @{ displayName = $DisplayName } } | ConvertTo-Json -Compress + $PasswordResult = New-GraphPOSTRequest -uri "https://graph.microsoft.com/v1.0/applications/$ObjectId/addPassword" -body $PasswordBody -type POST -NoAuthCheck $true -AsApp $true + $SecretText = $PasswordResult.secretText + Write-Information "[SSO-Secret] Client secret created on objectId $ObjectId" + } catch { + $SecretAttempt++ + $LastException = $_ + Write-Warning "[SSO-Secret] Secret creation attempt $SecretAttempt/$MaxRetries failed: $($_.Exception.Message)" + if ($SecretAttempt -lt $MaxRetries) { + $Delay = $BackoffSchedule[[Math]::Min($SecretAttempt - 1, $BackoffSchedule.Count - 1)] + Start-Sleep -Seconds $Delay + } + } + } + + if (-not $SecretText) { + $InnerMessage = if ($LastException) { $LastException.Exception.Message } else { 'unknown error' } + throw "Failed to create client secret for CIPP-SSO after $MaxRetries attempts: $InnerMessage" + } + + return $SecretText +} diff --git a/Modules/CIPPCore/Public/Authentication/New-CIPPSSOApp.ps1 b/Modules/CIPPCore/Public/Authentication/New-CIPPSSOApp.ps1 index d8291eacd06b..39a461977343 100644 --- a/Modules/CIPPCore/Public/Authentication/New-CIPPSSOApp.ps1 +++ b/Modules/CIPPCore/Public/Authentication/New-CIPPSSOApp.ps1 @@ -6,8 +6,9 @@ function New-CIPPSSOApp { Creates a new or updates an existing Entra ID app registration for CIPP-SSO with openid, profile, and email delegated permissions. If ExistingAppId is provided, looks up that specific app by clientId. If the app no longer exists in the tenant, - creates a new one. Generates a client secret and returns the details needed to - configure EasyAuth. + creates a new one. Does NOT create a client secret — call Add-CIPPSSOAppSecret + for that as a separate step so the AppId can be persisted before the (sometimes + flaky) secret creation runs. #> [CmdletBinding()] param( @@ -120,37 +121,12 @@ function New-CIPPSSOApp { Write-Warning "[SSO-App] App management policy update failed (secret creation may still work): $($_.Exception.Message)" } - # Create client secret with retry - $SecretText = $null - $SecretAttempt = 0 - $MaxSecretRetries = 5 - while ($SecretAttempt -lt $MaxSecretRetries -and -not $SecretText) { - try { - $PasswordBody = '{"passwordCredential":{"displayName":"CIPP-SSO-Secret"}}' - $PasswordResult = New-GraphPOSTRequest -uri "https://graph.microsoft.com/v1.0/applications/$AppObjectId/addPassword" -body $PasswordBody -type POST -NoAuthCheck $true -AsApp $true - $SecretText = $PasswordResult.secretText - Write-Information "[SSO-App] Client secret created" - } catch { - $SecretAttempt++ - Write-Warning "[SSO-App] Secret creation attempt $SecretAttempt/$MaxSecretRetries failed: $($_.Exception.Message)" - if ($SecretAttempt -lt $MaxSecretRetries) { - $Delay = @(2, 5, 10, 15, 30)[$SecretAttempt - 1] - Start-Sleep -Seconds $Delay - } - } - } - - if (-not $SecretText) { - throw "Failed to create client secret for $AppDisplayName after $MaxSecretRetries attempts" - } - return [PSCustomObject]@{ - AppId = $AppClientId - ObjectId = $AppObjectId - ClientSecret = $SecretText - TenantId = $env:TenantID - DisplayName = $AppDisplayName - State = $State - MultiTenant = $MultiTenant + AppId = $AppClientId + ObjectId = $AppObjectId + TenantId = $env:TenantID + DisplayName = $AppDisplayName + State = $State + MultiTenant = $MultiTenant } } diff --git a/Modules/CIPPCore/Public/Authentication/Set-CIPPSSOStoredCredentials.ps1 b/Modules/CIPPCore/Public/Authentication/Set-CIPPSSOStoredCredentials.ps1 new file mode 100644 index 000000000000..8ee4bd1456c1 --- /dev/null +++ b/Modules/CIPPCore/Public/Authentication/Set-CIPPSSOStoredCredentials.ps1 @@ -0,0 +1,54 @@ +function Set-CIPPSSOStoredCredentials { + <# + .SYNOPSIS + Persists CIPP-SSO credentials to Key Vault (or the DevSecrets table in dev mode). + .DESCRIPTION + Writes whichever of -AppId / -AppSecret / -MultiTenant were supplied. Pass only + the values you actually want to update — e.g. Repair passes only -AppSecret, + Create passes all three. + #> + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] + [CmdletBinding()] + param( + [Parameter(Mandatory = $false)][string]$AppId, + [Parameter(Mandatory = $false)][string]$AppSecret, + [Parameter(Mandatory = $false)][object]$MultiTenant + ) + + $IsDev = $env:AzureWebJobsStorage -eq 'UseDevelopmentStorage=true' -or $env:NonLocalHostAzurite -eq 'true' + + if ($IsDev) { + $DevSecretsTable = Get-CIPPTable -tablename 'DevSecrets' + $Secret = Get-CIPPAzDataTableEntity @DevSecretsTable -Filter "PartitionKey eq 'SSO' and RowKey eq 'SSO'" -ErrorAction SilentlyContinue + if (-not $Secret) { $Secret = [PSCustomObject]@{} } + $Secret | Add-Member -MemberType NoteProperty -Name 'PartitionKey' -Value 'SSO' -Force + $Secret | Add-Member -MemberType NoteProperty -Name 'RowKey' -Value 'SSO' -Force + if ($AppId) { $Secret | Add-Member -MemberType NoteProperty -Name 'SSOAppId' -Value $AppId -Force } + if ($AppSecret) { $Secret | Add-Member -MemberType NoteProperty -Name 'SSOAppSecret' -Value $AppSecret -Force } + if ($PSBoundParameters.ContainsKey('MultiTenant')) { + $Secret | Add-Member -MemberType NoteProperty -Name 'SSOMultiTenant' -Value ([string]([bool]$MultiTenant)) -Force + } + Add-CIPPAzDataTableEntity @DevSecretsTable -Entity $Secret -Force | Out-Null + return + } + + $KV = $env:WEBSITE_DEPLOYMENT_ID + $VaultName = if ($KV) { ($KV -split '-')[0] } else { $null } + if (-not $VaultName) { throw 'Cannot determine Key Vault name from WEBSITE_DEPLOYMENT_ID' } + + if ($AppId) { + $ExistingAppIdSecret = $null + try { $ExistingAppIdSecret = Get-CippKeyVaultSecret -VaultName $VaultName -Name 'SSOAppId' -AsPlainText -ErrorAction Stop } catch { } + if (-not $ExistingAppIdSecret -or $ExistingAppIdSecret -ne $AppId) { + Set-CippKeyVaultSecret -VaultName $VaultName -Name 'SSOAppId' -SecretValue (ConvertTo-SecureString -String $AppId -AsPlainText -Force) + } + } + + if ($AppSecret) { + Set-CippKeyVaultSecret -VaultName $VaultName -Name 'SSOAppSecret' -SecretValue (ConvertTo-SecureString -String $AppSecret -AsPlainText -Force) + } + + if ($PSBoundParameters.ContainsKey('MultiTenant')) { + Set-CippKeyVaultSecret -VaultName $VaultName -Name 'SSOMultiTenant' -SecretValue (ConvertTo-SecureString -String ([string]([bool]$MultiTenant)) -AsPlainText -Force) + } +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecSSOSetup.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecSSOSetup.ps1 index 076fa3c2610e..f693c9d6648a 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecSSOSetup.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecSSOSetup.ps1 @@ -14,6 +14,37 @@ function Invoke-ExecSSOSetup { $Action = $Request.Body.Action ?? $Request.Query.Action ?? 'Status' $MigrationTable = Get-CIPPTable -tablename 'SSOMigration' + # Resolve the redirect URI once for any action that needs it + $ResolveTargetUrl = { + param($BodyUrl) + if ($BodyUrl) { return $BodyUrl } + $FromHeader = $Request.Headers.origin ?? $Request.Headers.referer?.TrimEnd('/') + if ($FromHeader) { return $FromHeader } + return "https://$($env:WEBSITE_HOSTNAME)" + } + + # Save a row to the migration table while preserving fields that aren't being updated + $SaveMigrationRow = { + param([hashtable]$Updates) + $Existing = Get-CIPPAzDataTableEntity @MigrationTable -Filter "PartitionKey eq 'SSO' and RowKey eq 'MigrationConfig'" -ErrorAction SilentlyContinue + $Row = @{ + PartitionKey = 'SSO' + RowKey = 'MigrationConfig' + } + # Preserve existing fields + if ($Existing) { + foreach ($Prop in $Existing.PSObject.Properties) { + if ($Prop.Name -notin @('PartitionKey', 'RowKey', 'Timestamp', 'ETag', 'odata.etag')) { + $Row[$Prop.Name] = $Prop.Value + } + } + } + # Apply updates on top + foreach ($Key in $Updates.Keys) { $Row[$Key] = $Updates[$Key] } + $Row['LastChecked'] = (Get-Date).ToUniversalTime().ToString('o') + Add-CIPPAzDataTableEntity @MigrationTable -Entity $Row -Force | Out-Null + } + switch ($Action) { 'Status' { # Read live EasyAuth config from the platform-injected env var when available @@ -32,10 +63,19 @@ function Invoke-ExecSSOSetup { $AllowedApps = @($AAD.validation.defaultAuthorizationPolicy.allowedApplications) $ExcludedPaths = @($Config.globalValidation.excludedPaths) + # Surface migration-table state for the live AppId so the UI can offer Repair + # if the migration row matches the live ClientId AND is in a partial state. + # If the migration row is stale (different AppId), defer to live EasyAuth = complete. + $Migration = Get-CIPPAzDataTableEntity @MigrationTable -Filter "PartitionKey eq 'SSO' and RowKey eq 'MigrationConfig'" -ErrorAction SilentlyContinue + $MigrationMatches = $Migration -and $Migration.AppId -and $Migration.AppId -eq $ClientId + $MigrationStatus = if ($MigrationMatches) { $Migration.Status } else { 'complete' } + $MigrationError = if ($MigrationMatches) { $Migration.LastError } else { '' } + $MigrationCanRepair = $MigrationMatches -and ($Migration.Status -in @('error', 'app_created', 'appid_stored')) + $Body = @{ Results = @{ configured = $true - status = 'complete' + status = $MigrationStatus appId = $ClientId multiTenant = $IsMultiTenant tenantId = $IssuerTenantId @@ -44,15 +84,35 @@ function Invoke-ExecSSOSetup { allowedApps = $AllowedApps excludedPaths = $ExcludedPaths easyAuthActive = $true + lastError = $MigrationError + canRepair = [bool]$MigrationCanRepair } } } else { - $Body = @{ Results = @{ configured = $false; status = 'none'; easyAuthActive = $false } } + # EasyAuth not active — fall through to the migration table so partial-state appId/error still surfaces + $Migration = Get-CIPPAzDataTableEntity @MigrationTable -Filter "PartitionKey eq 'SSO' and RowKey eq 'MigrationConfig'" -ErrorAction SilentlyContinue + if ($Migration) { + $Body = @{ + Results = @{ + configured = $true + status = $Migration.Status + appId = $Migration.AppId + multiTenant = [bool]($Migration.MultiTenant -eq 'true' -or $Migration.MultiTenant -eq 'True') + createdAt = $Migration.CreatedAt + lastChecked = $Migration.LastChecked + lastError = $Migration.LastError + easyAuthActive = $false + canRepair = [bool]($Migration.AppId -and ($Migration.Status -in @('error', 'app_created', 'appid_stored'))) + } + } + } else { + $Body = @{ Results = @{ configured = $false; status = 'none'; easyAuthActive = $false } } + } } } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API $APIName -message "Failed to parse EasyAuth config: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - $Body = @{ Results = @{ configured = $false; status = 'error'; error = $ErrorMessage.NormalizedError } } + $Body = @{ Results = @{ configured = $false; status = 'error'; lastError = $ErrorMessage.NormalizedError } } } } else { # Otherwise read from migration table @@ -64,10 +124,11 @@ function Invoke-ExecSSOSetup { configured = $true status = $Migration.Status appId = $Migration.AppId - multiTenant = [bool]($Migration.MultiTenant -eq 'true') + multiTenant = [bool]($Migration.MultiTenant -eq 'true' -or $Migration.MultiTenant -eq 'True') createdAt = $Migration.CreatedAt lastChecked = $Migration.LastChecked lastError = $Migration.LastError + canRepair = [bool]($Migration.AppId -and ($Migration.Status -in @('error', 'app_created', 'appid_stored'))) } } } else { @@ -76,30 +137,22 @@ function Invoke-ExecSSOSetup { } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API $APIName -message "Failed to get SSO status: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - $Body = @{ Results = @{ configured = $false; status = 'error'; error = $ErrorMessage.NormalizedError } } + $Body = @{ Results = @{ configured = $false; status = 'error'; lastError = $ErrorMessage.NormalizedError } } } } } 'Create' { $MultiTenant = [bool]($Request.Body.multiTenant) - $TargetUrl = $Request.Body.targetUrl - - # Determine redirect URI — prefer explicit targetUrl, fall back to current host - if (-not $TargetUrl) { - $TargetUrl = $Request.Headers.origin ?? $Request.Headers.referer?.TrimEnd('/') - } - if (-not $TargetUrl) { - $TargetUrl = "https://$($env:WEBSITE_HOSTNAME)" - } + $TargetUrl = & $ResolveTargetUrl $Request.Body.targetUrl try { # Check if already provisioned $Existing = Get-CIPPAzDataTableEntity @MigrationTable -Filter "PartitionKey eq 'SSO' and RowKey eq 'MigrationConfig'" -ErrorAction SilentlyContinue - if ($Existing -and $Existing.Status -eq 'complete') { + if ($Existing -and $Existing.Status -in @('secrets_stored', 'complete')) { $Body = @{ Results = @{ - message = 'SSO migration already completed.' + message = 'SSO app is already provisioned. Use Repair to refresh the secret or Recreate to start over.' appId = $Existing.AppId severity = 'info' } @@ -107,115 +160,94 @@ function Invoke-ExecSSOSetup { break } - # If we have an existing record that isn't complete, pick up from where we left off - $AppId = $Existing.AppId - $AppSecret = $null + # Pick up from where we left off if we have a partial record + $ExistingAppId = $Existing.AppId - # Step 1: Create/update the app registration (idempotent) - # Pass stored AppId so we look up by clientId rather than name + # --- Step 1: Create or update the app registration (no secret yet) --- $SSOAppParams = @{ RedirectUri = $TargetUrl MultiTenant = $MultiTenant } - if ($AppId) { $SSOAppParams.ExistingAppId = $AppId } + if ($ExistingAppId) { $SSOAppParams.ExistingAppId = $ExistingAppId } $SSOApp = New-CIPPSSOApp @SSOAppParams $AppId = $SSOApp.AppId - $AppSecret = $SSOApp.ClientSecret + $ObjectId = $SSOApp.ObjectId Write-LogMessage -API $APIName -headers $Headers -message "CIPP-SSO app $($SSOApp.State): $AppId" -sev Info - # Save progress immediately - $MigrationRow = @{ - PartitionKey = 'SSO' - RowKey = 'MigrationConfig' - AppId = $AppId - MultiTenant = [string]$MultiTenant - RedirectUri = $TargetUrl - Status = 'app_created' - CreatedAt = $Existing.CreatedAt ?? (Get-Date).ToUniversalTime().ToString('o') - LastChecked = (Get-Date).ToUniversalTime().ToString('o') - LastError = '' + # --- Step 2: Persist AppId immediately so a later secret failure doesn't lose it --- + & $SaveMigrationRow @{ + AppId = $AppId + ObjectId = $ObjectId + MultiTenant = [string]$MultiTenant + RedirectUri = $TargetUrl + Status = 'app_created' + CreatedAt = $Existing.CreatedAt ?? (Get-Date).ToUniversalTime().ToString('o') + LastError = '' } - Add-CIPPAzDataTableEntity @MigrationTable -Entity $MigrationRow -Force | Out-Null - - $KV = $env:WEBSITE_DEPLOYMENT_ID - $VaultName = if ($KV) { ($KV -split '-')[0] } else { $null } - - if ($env:AzureWebJobsStorage -eq 'UseDevelopmentStorage=true' -or $env:NonLocalHostAzurite -eq 'true') { - # Dev mode — store in DevSecrets table - $DevSecretsTable = Get-CIPPTable -tablename 'DevSecrets' - $Secret = Get-CIPPAzDataTableEntity @DevSecretsTable -Filter "PartitionKey eq 'SSO' and RowKey eq 'SSO'" -ErrorAction SilentlyContinue - if (-not $Secret) { $Secret = [PSCustomObject]@{} } - $Secret | Add-Member -MemberType NoteProperty -Name 'PartitionKey' -Value 'SSO' -Force - $Secret | Add-Member -MemberType NoteProperty -Name 'RowKey' -Value 'SSO' -Force - $Secret | Add-Member -MemberType NoteProperty -Name 'SSOAppId' -Value $AppId -Force - $Secret | Add-Member -MemberType NoteProperty -Name 'SSOMultiTenant' -Value ([string]$MultiTenant) -Force - if ($AppSecret) { - $Secret | Add-Member -MemberType NoteProperty -Name 'SSOAppSecret' -Value $AppSecret -Force - } - Add-CIPPAzDataTableEntity @DevSecretsTable -Entity $Secret -Force | Out-Null - Write-Information '[SSO-Setup] Stored SSO credentials in DevSecrets table' - } else { - # Production — store in Key Vault - if (-not $VaultName) { - throw 'Cannot determine Key Vault name from WEBSITE_DEPLOYMENT_ID' - } - - # Step 2: Store AppId in KV (idempotent — Set overwrites) - $ExistingAppIdSecret = $null - try { - $ExistingAppIdSecret = Get-CippKeyVaultSecret -VaultName $VaultName -Name 'SSOAppId' -AsPlainText -ErrorAction Stop - } catch { } - - if (-not $ExistingAppIdSecret -or $ExistingAppIdSecret -ne $AppId) { - Set-CippKeyVaultSecret -VaultName $VaultName -Name 'SSOAppId' -SecretValue (ConvertTo-SecureString -String $AppId -AsPlainText -Force) - Write-Information "[SSO-Setup] Stored SSOAppId in Key Vault" - } - # Update status - $UpdateRow = @{ - PartitionKey = 'SSO' - RowKey = 'MigrationConfig' - Status = 'appid_stored' - LastChecked = (Get-Date).ToUniversalTime().ToString('o') + # --- Step 3: Store AppId + MultiTenant flag in KV (still no secret) --- + try { + Set-CIPPSSOStoredCredentials -AppId $AppId -MultiTenant $MultiTenant + Write-Information '[SSO-Setup] AppId and MultiTenant flag stored' + + # Best-effort: stash TenantID in KV if missing (was previously inline) + if (-not ($env:AzureWebJobsStorage -eq 'UseDevelopmentStorage=true' -or $env:NonLocalHostAzurite -eq 'true')) { + $KV = $env:WEBSITE_DEPLOYMENT_ID + $VaultName = if ($KV) { ($KV -split '-')[0] } else { $null } + if ($VaultName -and $env:TenantID) { + $ExistingTenantId = $null + try { $ExistingTenantId = Get-CippKeyVaultSecret -VaultName $VaultName -Name 'TenantID' -AsPlainText -ErrorAction Stop } catch { } + if (-not $ExistingTenantId) { + Set-CippKeyVaultSecret -VaultName $VaultName -Name 'TenantID' -SecretValue (ConvertTo-SecureString -String $env:TenantID -AsPlainText -Force) + Write-Information '[SSO-Setup] Stored TenantID in Key Vault (was missing)' + } + } } - Add-CIPPAzDataTableEntity @MigrationTable -Entity $UpdateRow -Force | Out-Null - # Step 3: Store AppSecret in KV - if ($AppSecret) { - Set-CippKeyVaultSecret -VaultName $VaultName -Name 'SSOAppSecret' -SecretValue (ConvertTo-SecureString -String $AppSecret -AsPlainText -Force) - Write-Information "[SSO-Setup] Stored SSOAppSecret in Key Vault" - } + & $SaveMigrationRow @{ Status = 'appid_stored' } + } catch { + $StoreError = Get-CippException -Exception $_ + Write-LogMessage -API $APIName -headers $Headers -message "Failed to store SSO AppId: $($StoreError.NormalizedError)" -sev Error -LogData $StoreError + & $SaveMigrationRow @{ Status = 'error'; LastError = "Failed to store AppId: $($StoreError.NormalizedError)" } + throw + } - # Step 4: Verify TenantID exists in KV (should already be there from SAM setup) - $ExistingTenantId = $null - try { - $ExistingTenantId = Get-CippKeyVaultSecret -VaultName $VaultName -Name 'TenantID' -AsPlainText -ErrorAction Stop - } catch { } + # --- Step 4: Create the client secret (may legitimately fail; Repair can resume) --- + $AppSecret = $null + try { + $AppSecret = Add-CIPPSSOAppSecret -ObjectId $ObjectId + } catch { + $SecretError = Get-CippException -Exception $_ + Write-LogMessage -API $APIName -headers $Headers -message "SSO secret creation failed (AppId preserved, use Repair): $($SecretError.NormalizedError)" -sev Error -LogData $SecretError + & $SaveMigrationRow @{ Status = 'error'; LastError = $SecretError.NormalizedError } - if (-not $ExistingTenantId) { - Set-CippKeyVaultSecret -VaultName $VaultName -Name 'TenantID' -SecretValue (ConvertTo-SecureString -String $env:TenantID -AsPlainText -Force) - Write-Information "[SSO-Setup] Stored TenantID in Key Vault (was missing)" + $StatusCode = [HttpStatusCode]::OK + $Body = @{ + Results = @{ + message = "SSO app created (AppId: $AppId) but client secret creation failed. Use Repair to retry — the AppId is preserved." + appId = $AppId + severity = 'warning' + canRepair = $true + lastError = $SecretError.NormalizedError + } } - - # Step 5: Store MultiTenant flag in KV (used for initial EasyAuth setup on startup) - Set-CippKeyVaultSecret -VaultName $VaultName -Name 'SSOMultiTenant' -SecretValue (ConvertTo-SecureString -String ([string]$MultiTenant) -AsPlainText -Force) - Write-Information "[SSO-Setup] Stored SSOMultiTenant=$MultiTenant in Key Vault" + break } - # Mark migration as secrets_stored - $FinalRow = @{ - PartitionKey = 'SSO' - RowKey = 'MigrationConfig' - AppId = $AppId - MultiTenant = [string]$MultiTenant - RedirectUri = $TargetUrl - Status = 'secrets_stored' - CreatedAt = $Existing.CreatedAt ?? (Get-Date).ToUniversalTime().ToString('o') - LastChecked = (Get-Date).ToUniversalTime().ToString('o') - LastError = '' + # --- Step 5: Store the secret --- + try { + Set-CIPPSSOStoredCredentials -AppSecret $AppSecret + Write-Information '[SSO-Setup] AppSecret stored' + } catch { + $StoreError = Get-CippException -Exception $_ + Write-LogMessage -API $APIName -headers $Headers -message "Failed to store SSO secret: $($StoreError.NormalizedError)" -sev Error -LogData $StoreError + & $SaveMigrationRow @{ Status = 'error'; LastError = "Secret created but storage failed: $($StoreError.NormalizedError)" } + throw } - Add-CIPPAzDataTableEntity @MigrationTable -Entity $FinalRow -Force | Out-Null + + # --- Step 6: Mark migration as secrets_stored --- + & $SaveMigrationRow @{ Status = 'secrets_stored'; LastError = '' } Write-LogMessage -API $APIName -headers $Headers -message "SSO migration credentials stored for app $AppId" -sev Info $Body = @{ @@ -230,21 +262,111 @@ function Invoke-ExecSSOSetup { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API $APIName -headers $Headers -message "SSO setup failed: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - # Save error state so the scheduled task can retry - $ErrorRow = @{ - PartitionKey = 'SSO' - RowKey = 'MigrationConfig' - Status = 'error' - LastChecked = (Get-Date).ToUniversalTime().ToString('o') - LastError = $ErrorMessage.NormalizedError - } - try { Add-CIPPAzDataTableEntity @MigrationTable -Entity $ErrorRow -Force | Out-Null } catch { } + # Migration row already has the most accurate Status/LastError from the inner catches; only write if nothing was written + try { + $Existing = Get-CIPPAzDataTableEntity @MigrationTable -Filter "PartitionKey eq 'SSO' and RowKey eq 'MigrationConfig'" -ErrorAction SilentlyContinue + if (-not $Existing -or $Existing.Status -ne 'error') { + & $SaveMigrationRow @{ Status = 'error'; LastError = $ErrorMessage.NormalizedError } + } + } catch { } $StatusCode = [HttpStatusCode]::InternalServerError $Body = @{ Results = "SSO setup failed: $($ErrorMessage.NormalizedError)" } } } + 'Repair' { + # Picks up from any partial state — adds a new secret to the existing AppId and stores it. + try { + $Existing = Get-CIPPAzDataTableEntity @MigrationTable -Filter "PartitionKey eq 'SSO' and RowKey eq 'MigrationConfig'" -ErrorAction SilentlyContinue + + # Fall back to live EasyAuth config when the migration table is empty (e.g. forced-migration flow) + if ((-not $Existing -or -not $Existing.AppId) -and $env:CIPPNG -and $env:WEBSITE_AUTH_V2_CONFIG_JSON) { + $LiveConfig = $env:WEBSITE_AUTH_V2_CONFIG_JSON | ConvertFrom-Json -ErrorAction SilentlyContinue + $LiveAppId = $LiveConfig.identityProviders.azureActiveDirectory.registration.clientId + if ($LiveAppId) { + $Existing = [PSCustomObject]@{ AppId = $LiveAppId; MultiTenant = 'false' } + } + } + + if (-not $Existing -or -not $Existing.AppId) { + $StatusCode = [HttpStatusCode]::BadRequest + $Body = @{ Results = 'No SSO app to repair. Use Create to provision one.' } + break + } + + $AppId = $Existing.AppId + + # Look up the ObjectId — we may have stored it, or we need to fetch it from Graph + $ObjectId = $Existing.ObjectId + if (-not $ObjectId) { + $AppResponse = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/applications(appId='$AppId')?`$select=id" -NoAuthCheck $true -AsApp $true + $ObjectId = $AppResponse.id + } + + # Create a fresh secret on the existing app + $AppSecret = Add-CIPPSSOAppSecret -ObjectId $ObjectId + + # Persist it + $MultiTenantFlag = [bool]($Existing.MultiTenant -eq 'true' -or $Existing.MultiTenant -eq 'True') + Set-CIPPSSOStoredCredentials -AppId $AppId -AppSecret $AppSecret -MultiTenant $MultiTenantFlag + + & $SaveMigrationRow @{ + AppId = $AppId + ObjectId = $ObjectId + MultiTenant = [string]$MultiTenantFlag + Status = 'secrets_stored' + LastError = '' + } + + Write-LogMessage -API $APIName -headers $Headers -message "SSO app repaired — new secret stored for $AppId" -sev Info + $Body = @{ + Results = @{ + message = 'CIPP-SSO repaired. A new client secret was created and stored. EasyAuth will pick it up on next restart.' + appId = $AppId + severity = 'success' + } + } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API $APIName -headers $Headers -message "SSO repair failed: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + try { & $SaveMigrationRow @{ Status = 'error'; LastError = $ErrorMessage.NormalizedError } } catch { } + $StatusCode = [HttpStatusCode]::InternalServerError + $Body = @{ Results = "SSO repair failed: $($ErrorMessage.NormalizedError)" } + } + } + + 'Recreate' { + # Clears the migration record so the next Create provisions a brand new app + # (the previous app is left orphaned in the tenant — admin can delete manually if desired). + try { + $Existing = Get-CIPPAzDataTableEntity @MigrationTable -Filter "PartitionKey eq 'SSO' and RowKey eq 'MigrationConfig'" -ErrorAction SilentlyContinue + $PreviousAppId = $Existing.AppId + + if ($Existing) { + Remove-AzDataTableEntity @MigrationTable -Entity $Existing -Force | Out-Null + Write-LogMessage -API $APIName -headers $Headers -message "SSO migration record cleared (previous AppId: $PreviousAppId). Use Create to provision a new app." -sev Info + } + + $Body = @{ + Results = @{ + message = if ($PreviousAppId) { + "Previous SSO record cleared. The old app registration ($PreviousAppId) is still in your tenant — delete it manually from Entra if you no longer want it. Click Create SSO App to provision a fresh one." + } else { + 'No SSO record to clear. Click Create SSO App to provision a new app.' + } + previousAppId = $PreviousAppId + severity = 'success' + } + } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API $APIName -headers $Headers -message "SSO recreate failed: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::InternalServerError + $Body = @{ Results = "SSO recreate failed: $($ErrorMessage.NormalizedError)" } + } + } + 'Update' { # Update existing SSO app configuration (e.g. switch single ↔ multi-tenant) try { @@ -264,13 +386,7 @@ function Invoke-ExecSSOSetup { } $MultiTenant = [bool]($Request.Body.multiTenant) - $TargetUrl = $Request.Body.targetUrl - if (-not $TargetUrl) { - $TargetUrl = $Request.Headers.origin ?? $Request.Headers.referer?.TrimEnd('/') - } - if (-not $TargetUrl) { - $TargetUrl = "https://$($env:WEBSITE_HOSTNAME)" - } + $TargetUrl = & $ResolveTargetUrl $Request.Body.targetUrl $SignInAudience = if ($MultiTenant) { 'AzureADMultipleOrgs' } else { 'AzureADMyOrg' } $CallbackUri = $TargetUrl.TrimEnd('/') + '/.auth/login/aad/callback' @@ -289,34 +405,17 @@ function Invoke-ExecSSOSetup { New-GraphPOSTRequest -uri "https://graph.microsoft.com/v1.0/applications/$($AppResponse.id)" -body $PatchBody -type PATCH -NoAuthCheck $true -AsApp $true # Update migration table - $UpdateRow = @{ - PartitionKey = 'SSO' - RowKey = 'MigrationConfig' - AppId = $Existing.AppId - MultiTenant = [string]$MultiTenant - RedirectUri = $TargetUrl - Status = $Existing.Status - CreatedAt = $Existing.CreatedAt - LastChecked = (Get-Date).ToUniversalTime().ToString('o') - LastError = '' + & $SaveMigrationRow @{ + AppId = $Existing.AppId + MultiTenant = [string]$MultiTenant + RedirectUri = $TargetUrl + LastError = '' } - Add-CIPPAzDataTableEntity @MigrationTable -Entity $UpdateRow -Force | Out-Null Write-LogMessage -API $APIName -headers $Headers -message "SSO app updated: multiTenant=$MultiTenant, audience=$SignInAudience" -sev Info # Update SSOMultiTenant in KV so initial EasyAuth setup stays in sync - $KV = $env:WEBSITE_DEPLOYMENT_ID - $VaultName = if ($KV) { ($KV -split '-')[0] } else { $null } - if ($env:AzureWebJobsStorage -eq 'UseDevelopmentStorage=true' -or $env:NonLocalHostAzurite -eq 'true') { - $DevSecretsTable = Get-CIPPTable -tablename 'DevSecrets' - $Secret = Get-CIPPAzDataTableEntity @DevSecretsTable -Filter "PartitionKey eq 'SSO' and RowKey eq 'SSO'" -ErrorAction SilentlyContinue - if ($Secret) { - $Secret | Add-Member -MemberType NoteProperty -Name 'SSOMultiTenant' -Value ([string]$MultiTenant) -Force - Add-CIPPAzDataTableEntity @DevSecretsTable -Entity $Secret -Force | Out-Null - } - } elseif ($VaultName) { - Set-CippKeyVaultSecret -VaultName $VaultName -Name 'SSOMultiTenant' -SecretValue (ConvertTo-SecureString -String ([string]$MultiTenant) -AsPlainText -Force) - } + Set-CIPPSSOStoredCredentials -MultiTenant $MultiTenant # Update EasyAuth ARM config on the App Service (issuer URL + allowed tenants) try { @@ -360,42 +459,19 @@ function Invoke-ExecSSOSetup { } # Get the app object ID - $AppResponse = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/applications(appId='$($Existing.AppId)')?`$select=id" -NoAuthCheck $true -AsApp $true - - # Create new secret - $PasswordBody = '{"passwordCredential":{"displayName":"CIPP-SSO-Secret"}}' - $PasswordResult = New-GraphPOSTRequest -uri "https://graph.microsoft.com/v1.0/applications/$($AppResponse.id)/addPassword" -body $PasswordBody -type POST -NoAuthCheck $true -AsApp $true - $NewSecret = $PasswordResult.secretText - - if (-not $NewSecret) { - throw 'Failed to create new client secret' + $ObjectId = $Existing.ObjectId + if (-not $ObjectId) { + $AppResponse = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/applications(appId='$($Existing.AppId)')?`$select=id" -NoAuthCheck $true -AsApp $true + $ObjectId = $AppResponse.id } + # Create new secret using the same retry helper + $NewSecret = Add-CIPPSSOAppSecret -ObjectId $ObjectId + # Store new secret - $KV = $env:WEBSITE_DEPLOYMENT_ID - $VaultName = if ($KV) { ($KV -split '-')[0] } else { $null } - - if ($env:AzureWebJobsStorage -eq 'UseDevelopmentStorage=true' -or $env:NonLocalHostAzurite -eq 'true') { - $DevSecretsTable = Get-CIPPTable -tablename 'DevSecrets' - $Secret = Get-CIPPAzDataTableEntity @DevSecretsTable -Filter "PartitionKey eq 'SSO' and RowKey eq 'SSO'" -ErrorAction SilentlyContinue - if (-not $Secret) { $Secret = [PSCustomObject]@{} } - $Secret | Add-Member -MemberType NoteProperty -Name 'PartitionKey' -Value 'SSO' -Force - $Secret | Add-Member -MemberType NoteProperty -Name 'RowKey' -Value 'SSO' -Force - $Secret | Add-Member -MemberType NoteProperty -Name 'SSOAppSecret' -Value $NewSecret -Force - Add-CIPPAzDataTableEntity @DevSecretsTable -Entity $Secret -Force | Out-Null - } else { - if (-not $VaultName) { throw 'Cannot determine Key Vault name from WEBSITE_DEPLOYMENT_ID' } - Set-CippKeyVaultSecret -VaultName $VaultName -Name 'SSOAppSecret' -SecretValue (ConvertTo-SecureString -String $NewSecret -AsPlainText -Force) - } + Set-CIPPSSOStoredCredentials -AppSecret $NewSecret - # Update last checked - $UpdateRow = @{ - PartitionKey = 'SSO' - RowKey = 'MigrationConfig' - LastChecked = (Get-Date).ToUniversalTime().ToString('o') - LastError = '' - } - Add-CIPPAzDataTableEntity @MigrationTable -Entity $UpdateRow -Force | Out-Null + & $SaveMigrationRow @{ LastError = '' } Write-LogMessage -API $APIName -headers $Headers -message "SSO app secret rotated for $($Existing.AppId)" -sev Info $Body = @{ @@ -426,20 +502,26 @@ function Invoke-ExecSSOSetup { $TargetUrl = "https://$($env:WEBSITE_HOSTNAME)" try { - # Check if we already have SSO credentials from a previous partial run - $KV = $env:WEBSITE_DEPLOYMENT_ID - $VaultName = if ($KV) { ($KV -split '-')[0] } else { $null } - $ExistingAppId = $null - - if ($env:AzureWebJobsStorage -eq 'UseDevelopmentStorage=true' -or $env:NonLocalHostAzurite -eq 'true') { - $DevSecretsTable = Get-CIPPTable -tablename 'DevSecrets' - $DevSecret = Get-CIPPAzDataTableEntity @DevSecretsTable -Filter "PartitionKey eq 'SSO' and RowKey eq 'SSO'" -ErrorAction SilentlyContinue - $ExistingAppId = $DevSecret.SSOAppId - } elseif ($VaultName) { - try { $ExistingAppId = Get-CippKeyVaultSecret -VaultName $VaultName -Name 'SSOAppId' -AsPlainText -ErrorAction Stop } catch { } + # Check if we have an in-progress migration record (so secret-only retries reuse the AppId) + $Existing = Get-CIPPAzDataTableEntity @MigrationTable -Filter "PartitionKey eq 'SSO' and RowKey eq 'MigrationConfig'" -ErrorAction SilentlyContinue + $ExistingAppId = $Existing.AppId + + # Also check KV / DevSecrets in case a previous partial run stored the AppId there + if (-not $ExistingAppId) { + if ($env:AzureWebJobsStorage -eq 'UseDevelopmentStorage=true' -or $env:NonLocalHostAzurite -eq 'true') { + $DevSecretsTable = Get-CIPPTable -tablename 'DevSecrets' + $DevSecret = Get-CIPPAzDataTableEntity @DevSecretsTable -Filter "PartitionKey eq 'SSO' and RowKey eq 'SSO'" -ErrorAction SilentlyContinue + $ExistingAppId = $DevSecret.SSOAppId + } else { + $KV = $env:WEBSITE_DEPLOYMENT_ID + $VaultName = if ($KV) { ($KV -split '-')[0] } else { $null } + if ($VaultName) { + try { $ExistingAppId = Get-CippKeyVaultSecret -VaultName $VaultName -Name 'SSOAppId' -AsPlainText -ErrorAction Stop } catch { } + } + } } - # Step 1: Create or update the customer's own CIPP-SSO app registration + # --- Step 1: Create or update the customer's own CIPP-SSO app registration (no secret yet) --- $SSOAppParams = @{ RedirectUri = $TargetUrl MultiTenant = $MultiTenant @@ -448,58 +530,50 @@ function Invoke-ExecSSOSetup { $SSOApp = New-CIPPSSOApp @SSOAppParams $AppId = $SSOApp.AppId - $AppSecret = $SSOApp.ClientSecret + $ObjectId = $SSOApp.ObjectId Write-LogMessage -API $APIName -headers $Headers -message "SSO migration: CIPP-SSO app $($SSOApp.State): $AppId" -sev Info - # Step 2: Store credentials - if ($env:AzureWebJobsStorage -eq 'UseDevelopmentStorage=true' -or $env:NonLocalHostAzurite -eq 'true') { - $DevSecretsTable = Get-CIPPTable -tablename 'DevSecrets' - $Secret = Get-CIPPAzDataTableEntity @DevSecretsTable -Filter "PartitionKey eq 'SSO' and RowKey eq 'SSO'" -ErrorAction SilentlyContinue - if (-not $Secret) { $Secret = [PSCustomObject]@{} } - $Secret | Add-Member -MemberType NoteProperty -Name 'PartitionKey' -Value 'SSO' -Force - $Secret | Add-Member -MemberType NoteProperty -Name 'RowKey' -Value 'SSO' -Force - $Secret | Add-Member -MemberType NoteProperty -Name 'SSOAppId' -Value $AppId -Force - $Secret | Add-Member -MemberType NoteProperty -Name 'SSOMultiTenant' -Value ([string]$MultiTenant) -Force - if ($AppSecret) { - $Secret | Add-Member -MemberType NoteProperty -Name 'SSOAppSecret' -Value $AppSecret -Force - } - Add-CIPPAzDataTableEntity @DevSecretsTable -Entity $Secret -Force | Out-Null - Write-Information '[SSO-Migrate] Stored SSO credentials in DevSecrets table' - } else { - if (-not $VaultName) { throw 'Cannot determine Key Vault name from WEBSITE_DEPLOYMENT_ID' } - - Set-CippKeyVaultSecret -VaultName $VaultName -Name 'SSOAppId' -SecretValue (ConvertTo-SecureString -String $AppId -AsPlainText -Force) - if ($AppSecret) { - Set-CippKeyVaultSecret -VaultName $VaultName -Name 'SSOAppSecret' -SecretValue (ConvertTo-SecureString -String $AppSecret -AsPlainText -Force) - } - Set-CippKeyVaultSecret -VaultName $VaultName -Name 'SSOMultiTenant' -SecretValue (ConvertTo-SecureString -String ([string]$MultiTenant) -AsPlainText -Force) - Write-Information "[SSO-Migrate] Stored SSO credentials in Key Vault ($VaultName)" + # --- Step 2: Persist AppId immediately --- + & $SaveMigrationRow @{ + AppId = $AppId + ObjectId = $ObjectId + MultiTenant = [string]$MultiTenant + RedirectUri = $TargetUrl + Status = 'app_created' + CreatedAt = $Existing.CreatedAt ?? (Get-Date).ToUniversalTime().ToString('o') + MigratedFrom = 'SWA' + LastError = '' + } + Set-CIPPSSOStoredCredentials -AppId $AppId -MultiTenant $MultiTenant + & $SaveMigrationRow @{ Status = 'appid_stored' } + + # --- Step 3: Create the client secret (with retry) --- + try { + $AppSecret = Add-CIPPSSOAppSecret -ObjectId $ObjectId + } catch { + $SecretError = Get-CippException -Exception $_ + Write-LogMessage -API $APIName -headers $Headers -message "SSO migration secret creation failed (AppId preserved, use Repair): $($SecretError.NormalizedError)" -sev Error -LogData $SecretError + & $SaveMigrationRow @{ Status = 'error'; LastError = $SecretError.NormalizedError } + $StatusCode = [HttpStatusCode]::InternalServerError + $Body = @{ Results = "SSO migration failed at secret creation: $($SecretError.NormalizedError) — use Repair from the SSO settings page once you can sign in." } + break } - # Step 3: Configure EasyAuth on the App Service + # --- Step 4: Store the secret --- + Set-CIPPSSOStoredCredentials -AppSecret $AppSecret + + # --- Step 5: Configure EasyAuth on the App Service --- Set-CIPPSSOEasyAuth -AppId $AppId -MultiTenant $MultiTenant -TenantId $env:TenantID -UseKvReferences - # Step 4: Remove the migration trigger env var + # --- Step 6: Remove the migration trigger env var --- Remove-CIPPMigrationAppSetting -SettingName 'CIPP_SSO_MIGRATION_APPID' - # Step 5: Track in migration table (for audit/status) - $MigrationRow = @{ - PartitionKey = 'SSO' - RowKey = 'MigrationConfig' - AppId = $AppId - MultiTenant = [string]$MultiTenant - RedirectUri = $TargetUrl - Status = 'complete' - CreatedAt = (Get-Date).ToUniversalTime().ToString('o') - LastChecked = (Get-Date).ToUniversalTime().ToString('o') - LastError = '' - MigratedFrom = 'SWA' - } - Add-CIPPAzDataTableEntity @MigrationTable -Entity $MigrationRow -Force | Out-Null + # --- Step 7: Mark complete --- + & $SaveMigrationRow @{ Status = 'complete'; LastError = '' } Write-LogMessage -API $APIName -headers $Headers -message "SSO migration complete: appId=$AppId, multiTenant=$MultiTenant" -sev Info - # Step 6: Restart to apply EasyAuth + # --- Step 8: Restart to apply EasyAuth --- Request-CIPPRestart -Reason 'SSO migration complete — EasyAuth configured with customer CIPP-SSO app' $Body = @{ @@ -513,6 +587,7 @@ function Invoke-ExecSSOSetup { } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API $APIName -headers $Headers -message "SSO migration failed: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + try { & $SaveMigrationRow @{ Status = 'error'; LastError = $ErrorMessage.NormalizedError } } catch { } $StatusCode = [HttpStatusCode]::InternalServerError $Body = @{ Results = "SSO migration failed: $($ErrorMessage.NormalizedError)" } } @@ -520,7 +595,7 @@ function Invoke-ExecSSOSetup { default { $StatusCode = [HttpStatusCode]::BadRequest - $Body = @{ Results = "Unknown action: $Action. Use 'Status', 'Create', or 'Update'." } + $Body = @{ Results = "Unknown action: $Action. Use 'Status', 'Create', 'Repair', 'Recreate', 'Update', 'RotateSecret', or 'Migrate'." } } } From c0f663ca436572f4a88571553d03a13b5ad9fc01 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Mon, 8 Jun 2026 22:49:25 +0200 Subject: [PATCH 171/202] fix: update role and fix variable casing in message trace function --- .../Tools/Invoke-ListMessageTrace.ps1 | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Tools/Invoke-ListMessageTrace.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Tools/Invoke-ListMessageTrace.ps1 index c6f2cd495d67..17976e179d6d 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Tools/Invoke-ListMessageTrace.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Tools/Invoke-ListMessageTrace.ps1 @@ -3,7 +3,7 @@ function Invoke-ListMessageTrace { .FUNCTIONALITY Entrypoint .ROLE - Exchange.TransportRule.Read + Exchange.Mailbox.Read .DESCRIPTION Traces email message delivery in Exchange Online, searchable by message ID, sender, recipient, and date range. #> @@ -11,6 +11,8 @@ function Invoke-ListMessageTrace { param($Request, $TriggerMetadata) $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + try { $TenantFilter = $Request.Body.tenantFilter @@ -51,13 +53,13 @@ function Invoke-ListMessageTrace { } if ($Request.Body.recipient) { - $Searchparams.Add('RecipientAddress', $($Request.Body.recipient.value ?? $Request.Body.recipient)) + $SearchParams.Add('RecipientAddress', $($Request.Body.recipient.value ?? $Request.Body.recipient)) } if ($Request.Body.sender) { - $Searchparams.Add('SenderAddress', $($Request.Body.sender.value ?? $Request.Body.sender)) + $SearchParams.Add('SenderAddress', $($Request.Body.sender.value ?? $Request.Body.sender)) } - $trace = if ($Request.Body.traceDetail) { + $Trace = if ($Request.Body.traceDetail) { $CmdParams = @{ MessageTraceId = $Request.Body.ID RecipientAddress = $Request.Body.recipient @@ -67,17 +69,18 @@ function Invoke-ListMessageTrace { Write-Information ($SearchParams | ConvertTo-Json) New-ExoRequest -TenantId $TenantFilter -Cmdlet 'Get-MessageTraceV2' -CmdParams $SearchParams | Select-Object MessageTraceId, Status, Subject, RecipientAddress, SenderAddress, @{ Name = 'Received'; Expression = { $_.Received.ToString('u') } }, FromIP, ToIP - Write-LogMessage -headers $Request.Headers -API $APIName -tenant $($TenantFilter) -message 'Executed message trace' -Sev 'Info' + Write-LogMessage -headers $Headers -API $APIName -tenant $($TenantFilter) -message 'Executed message trace' -Sev 'Info' } } catch { - Write-LogMessage -headers $Request.Headers -API $APINAME -tenant $($tenantfilter) -message "Failed executing messagetrace. Error: $($_.Exception.Message)" -Sev 'Error' - $trace = @{Status = "Failed to retrieve message trace $($_.Exception.Message)" } + Write-LogMessage -headers $Headers -API $APIName -tenant $($TenantFilter) -message "Failed executing Message Trace. Error: $($_.Exception.Message)" -Sev 'Error' + $Trace = @{Status = "Failed to retrieve message trace $($_.Exception.Message)" } + $StatusCode = [HttpStatusCode]::InternalServerError } return ([HttpResponseContext]@{ - StatusCode = [HttpStatusCode]::OK - Body = @($trace) + StatusCode = ($StatusCode ?? [HttpStatusCode]::OK) + Body = @($Trace) }) } From 3e33fdb15b67daa52c8bb0651b737193787d5371 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Mon, 8 Jun 2026 23:59:05 +0200 Subject: [PATCH 172/202] fix: more robust conversion for EnableAutoTrim setting --- .../Public/Standards/Invoke-CIPPStandardSPOVersionControl.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPOVersionControl.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPOVersionControl.ps1 index cc43fe2c4b0b..54cffe780164 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPOVersionControl.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPOVersionControl.ps1 @@ -49,7 +49,7 @@ function Invoke-CIPPStandardSPOVersionControl { return $true } - $DesiredAutoTrim = [bool]$Settings.EnableAutoTrim + $DesiredAutoTrim = [System.Convert]::ToBoolean($Settings.EnableAutoTrim) $DesiredMajorVersionLimit = [int]($Settings.MajorVersionLimit ?? 50) $DesiredExpireVersionsAfterDays = [int]($Settings.ExpireVersionsAfterDays ?? 0) From 3f9aab05b0c73759016aef39ba6cd62416b92d0b Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 9 Jun 2026 13:40:29 +0800 Subject: [PATCH 173/202] api version file fixes --- Config/version_latest.txt | 1 - .../Public/Authentication/Get-CippAllowedPermissions.ps1 | 2 +- .../Public/Entrypoints/Timer Functions/Start-CIPPStatsTimer.ps1 | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) delete mode 100644 Config/version_latest.txt diff --git a/Config/version_latest.txt b/Config/version_latest.txt deleted file mode 100644 index bb13e7c9bc64..000000000000 --- a/Config/version_latest.txt +++ /dev/null @@ -1 +0,0 @@ -10.4.2 diff --git a/Modules/CIPPCore/Public/Authentication/Get-CippAllowedPermissions.ps1 b/Modules/CIPPCore/Public/Authentication/Get-CippAllowedPermissions.ps1 index 0d0130689cd9..5a16b3c70a4e 100644 --- a/Modules/CIPPCore/Public/Authentication/Get-CippAllowedPermissions.ps1 +++ b/Modules/CIPPCore/Public/Authentication/Get-CippAllowedPermissions.ps1 @@ -23,7 +23,7 @@ function Get-CippAllowedPermissions { # Get all available permissions and base roles configuration - $Version = (Get-Content -Path (Join-Path $env:CIPPRootPath 'Config\version_latest.txt')).trim() + $Version = (Get-Content -Path (Join-Path $env:CIPPRootPath 'version_latest.txt')).trim() $BaseRoles = Get-Content -Path (Join-Path $env:CIPPRootPath 'Config\cipp-roles.json') | ConvertFrom-Json $DefaultRoles = @('superadmin', 'admin', 'editor', 'readonly', 'anonymous', 'authenticated') diff --git a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-CIPPStatsTimer.ps1 b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-CIPPStatsTimer.ps1 index 8bb757c4fbcf..7df1c6fa0b46 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-CIPPStatsTimer.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-CIPPStatsTimer.ps1 @@ -15,7 +15,7 @@ function Start-CIPPStatsTimer { $TenantCount = (Get-Tenants -IncludeAll).count - $APIVersion = Get-Content (Join-Path $env:CIPPRootPath 'Config\version_latest.txt') | Out-String + $APIVersion = Get-Content (Join-Path $env:CIPPRootPath 'version_latest.txt') | Out-String $Table = Get-CIPPTable -TableName Extensionsconfig try { $RawExt = (Get-CIPPAzDataTableEntity @Table).config | ConvertFrom-Json -Depth 10 -ErrorAction Stop From 2925643d2f15f3a6d551a85a60255fbaae4d5e9c Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 9 Jun 2026 17:09:03 +0800 Subject: [PATCH 174/202] push endpoint over to export job endpoint --- Config/CIPPTimers.json | 10 ++ .../Push-IntuneReportExportSubmit.ps1 | 66 ++++++++ .../Start-IntuneReportExportOrchestrator.ps1 | 57 +++++++ .../DBCache/Set-CIPPDBCacheDetectedApps.ps1 | 156 ++++++++++-------- 4 files changed, 224 insertions(+), 65 deletions(-) create mode 100644 Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-IntuneReportExportSubmit.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-IntuneReportExportOrchestrator.ps1 diff --git a/Config/CIPPTimers.json b/Config/CIPPTimers.json index 745034562ce3..b03f2070fa0b 100644 --- a/Config/CIPPTimers.json +++ b/Config/CIPPTimers.json @@ -237,6 +237,16 @@ "TZOffset": true, "IsSystem": true }, + { + "Id": "5e8a9b4c-2d6f-4a3e-b7c1-9d0e5f3a8b2c", + "Command": "Start-IntuneReportExportOrchestrator", + "Description": "Submit Intune report-export jobs ahead of nightly DB cache run", + "Cron": "0 0 2 * * *", + "Priority": 22, + "RunOnProcessor": true, + "TZOffset": true, + "IsSystem": true + }, { "Id": "9a7f8e6d-5c4b-3a2d-1e0f-9b8c7d6e5f4a", "Command": "Start-CIPPDBCacheOrchestrator", diff --git a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-IntuneReportExportSubmit.ps1 b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-IntuneReportExportSubmit.ps1 new file mode 100644 index 000000000000..bee6da069645 --- /dev/null +++ b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-IntuneReportExportSubmit.ps1 @@ -0,0 +1,66 @@ +function Push-IntuneReportExportSubmit { + <# + .SYNOPSIS + Submits an Intune report export job for a tenant and stores the job id. + .FUNCTIONALITY + Entrypoint + #> + [CmdletBinding()] + param($Item) + + $TenantFilter = $Item.TenantFilter + $ReportName = $Item.ReportName + + if (-not $TenantFilter -or -not $ReportName) { + Write-LogMessage -API 'IntuneReportExport' -message 'Missing TenantFilter or ReportName on activity item' -sev Error + return @{ Status = 'Failed'; Reason = 'MissingInput' } + } + + try { + $Select = switch ($ReportName) { + 'AppInvRawData' { + @( + 'ApplicationKey', 'ApplicationName', 'ApplicationPublisher', 'ApplicationVersion', + 'DeviceId', 'DeviceName', 'OSDescription', 'OSVersion', 'Platform', + 'UserId', 'UserName', 'EmailAddress' + ) + } + default { throw "Unknown Intune report '$ReportName'" } + } + + $Body = @{ + reportName = $ReportName + format = 'json' + localizationType = 'replaceLocalizableValues' + select = $Select + } | ConvertTo-Json -Depth 5 + + $Job = New-GraphPOSTRequest ` + -uri 'https://graph.microsoft.com/beta/deviceManagement/reports/exportJobs' ` + -tenantid $TenantFilter ` + -body $Body + + if (-not $Job.id) { throw "Intune returned no job id for $ReportName" } + + $JobsTable = Get-CIPPTable -tablename 'IntuneReportJobs' + $Existing = Get-CIPPAzDataTableEntity @JobsTable -Filter "PartitionKey eq '$TenantFilter' and RowKey eq '$ReportName'" + if ($Existing) { + Remove-AzDataTableEntity @JobsTable -Entity $Existing -Force -ErrorAction SilentlyContinue + } + + Add-CIPPAzDataTableEntity @JobsTable -Entity @{ + PartitionKey = $TenantFilter + RowKey = $ReportName + JobId = $Job.id + ReportName = $ReportName + SubmittedAt = ([DateTime]::UtcNow).ToString('o') + } -Force + + Write-LogMessage -API 'IntuneReportExport' -tenant $TenantFilter -message "Submitted $ReportName export job $($Job.id)" -sev Info + return @{ Status = 'Submitted'; JobId = $Job.id; ReportName = $ReportName; TenantFilter = $TenantFilter } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'IntuneReportExport' -tenant $TenantFilter -message "Failed to submit $ReportName export: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + return @{ Status = 'Failed'; ReportName = $ReportName; TenantFilter = $TenantFilter; Error = $ErrorMessage.NormalizedError } + } +} diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-IntuneReportExportOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-IntuneReportExportOrchestrator.ps1 new file mode 100644 index 000000000000..c772192523cd --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-IntuneReportExportOrchestrator.ps1 @@ -0,0 +1,57 @@ +function Start-IntuneReportExportOrchestrator { + <# + .SYNOPSIS + Submits Intune report-export jobs at 02:00 UTC ahead of the 03:00 cache run. + .FUNCTIONALITY + Entrypoint + #> + [CmdletBinding()] + param() + + try { + Write-LogMessage -API 'IntuneReportExport' -message 'Starting Intune report export submission' -sev Info + + $TenantList = Get-Tenants | Where-Object { $_.defaultDomainName -ne $null } + if ($TenantList.Count -eq 0) { + return + } + + $LicensedTenants = @(foreach ($Tenant in $TenantList) { + try { + if (Test-CIPPStandardLicense -StandardName 'IntuneReportExportSubmission' -TenantFilter $Tenant.defaultDomainName -Preset Intune -SkipLog) { + $Tenant + } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'IntuneReportExport' -tenant $Tenant.defaultDomainName -message "Intune license check failed: $($ErrorMessage.NormalizedError)" -sev Warning -LogData $ErrorMessage + } + }) + + if ($LicensedTenants.Count -eq 0) { + return + } + + $Queue = New-CippQueueEntry -Name 'Intune Report Export Submission' -TotalTasks $LicensedTenants.Count + + $Batch = foreach ($Tenant in $LicensedTenants) { + [PSCustomObject]@{ + FunctionName = 'IntuneReportExportSubmit' + TenantFilter = $Tenant.defaultDomainName + ReportName = 'AppInvRawData' + QueueId = $Queue.RowKey + QueueName = "Intune Export Submit - $($Tenant.defaultDomainName)" + } + } + + Start-CIPPOrchestrator -InputObject ([PSCustomObject]@{ + Batch = @($Batch) + OrchestratorName = 'IntuneReportExportOrchestrator' + SkipLog = $false + }) + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'IntuneReportExport' -message "Failed to start orchestration: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + throw + } +} diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheDetectedApps.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheDetectedApps.ps1 index 8783690f4b61..09c22d47b137 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheDetectedApps.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheDetectedApps.ps1 @@ -1,13 +1,14 @@ function Set-CIPPDBCacheDetectedApps { <# .SYNOPSIS - Caches all detected apps for a tenant, including devices that have each app + Caches detected apps using the AppInvRawData export submitted earlier, + enriched with the live /detectedApps catalog. .PARAMETER TenantFilter - The tenant to cache detected apps for + The tenant to cache detected apps for. .PARAMETER QueueId - The queue ID to update with total tasks (optional) + Optional queue ID for progress tracking. #> [CmdletBinding()] param( @@ -16,86 +17,111 @@ function Set-CIPPDBCacheDetectedApps { [string]$QueueId ) + $ReportName = 'AppInvRawData' + try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching detected apps' -sev Debug + $JobsTable = Get-CIPPTable -tablename 'IntuneReportJobs' + $JobRow = Get-CIPPAzDataTableEntity @JobsTable -Filter "PartitionKey eq '$TenantFilter' and RowKey eq '$ReportName'" - # Step 1: Get first page with noPaginate to avoid sequential chase, and read @odata.count - $FirstPageResult = New-GraphBulkRequest -Requests @( - [PSCustomObject]@{ - id = 'detectedApps-0' - method = 'GET' - url = 'deviceManagement/detectedApps' - } - ) -tenantid $TenantFilter -NoPaginateIds @('detectedApps-0') + if (-not $JobRow) { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "No $ReportName job submitted - skipping detected apps cache" -sev Info + return + } - $FirstResponse = ($FirstPageResult | Where-Object { $_.id -eq 'detectedApps-0' }).body - $TotalCount = $FirstResponse.'@odata.count' - $DetectedApps = [System.Collections.Generic.List[PSCustomObject]]::new() - foreach ($app in $FirstResponse.value) { $DetectedApps.Add($app) } + $JobId = $JobRow.JobId + if (-not $JobId) { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "IntuneReportJobs row missing JobId - removing" -sev Warning + Remove-AzDataTableEntity @JobsTable -Entity $JobRow -Force -ErrorAction SilentlyContinue + return + } - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "DetectedApps total count: $TotalCount, first page: $($DetectedApps.Count)" -sev Debug + try { + $Job = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/reports/exportJobs/$JobId" -tenantid $TenantFilter + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "$ReportName job $JobId not retrievable: $($ErrorMessage.NormalizedError)" -sev Warning -LogData $ErrorMessage + Remove-AzDataTableEntity @JobsTable -Entity $JobRow -Force -ErrorAction SilentlyContinue + return + } - # Step 2: If more pages exist, pre-calculate all skip offsets and fire as batches - if ($FirstResponse.'@odata.nextLink' -and $TotalCount -gt 50) { - $SkipRequests = [System.Collections.Generic.List[PSCustomObject]]::new() - for ($skip = 50; $skip -lt $TotalCount; $skip += 50) { - $SkipRequests.Add([PSCustomObject]@{ - id = "detectedApps-$skip" - method = 'GET' - url = "deviceManagement/detectedApps?`$skip=$skip" - }) + switch ($Job.status) { + 'completed' { } + 'failed' { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "$ReportName job $JobId failed" -sev Error + Remove-AzDataTableEntity @JobsTable -Entity $JobRow -Force -ErrorAction SilentlyContinue + return } - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Fetching $($SkipRequests.Count) remaining pages in bulk" -sev Debug - - # New-GraphBulkRequest auto-batches into groups of 20, NoPaginateIds prevents chasing empty nextLinks - $SkipResults = New-GraphBulkRequest -Requests @($SkipRequests) -tenantid $TenantFilter -NoPaginateIds @($SkipRequests.id) - - foreach ($Result in $SkipResults) { - if ($Result.status -eq 200 -and $Result.body.value) { - foreach ($app in $Result.body.value) { $DetectedApps.Add($app) } - } + default { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "$ReportName job $JobId still '$($Job.status)' - skipping" -sev Info + return } } - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Retrieved $($DetectedApps.Count) detected apps (expected $TotalCount)" -sev Debug - - if ($DetectedApps.Count -eq 0) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DetectedApps' -Data @() -AddCount + if (-not $Job.url) { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "$ReportName job $JobId completed but no url returned" -sev Error + Remove-AzDataTableEntity @JobsTable -Entity $JobRow -Force -ErrorAction SilentlyContinue return } - # Step 3: Bulk fetch managed devices for each app (unchanged from original) - $DeviceRequests = $DetectedApps | Where-Object { $_.id } | ForEach-Object { - [PSCustomObject]@{ - id = $_.id - method = 'GET' - url = "deviceManagement/detectedApps('$($_.id)')/managedDevices" - } + $ZipBytes = (Invoke-WebRequest -Uri $Job.url -UseBasicParsing -ErrorAction Stop).Content + if ($ZipBytes -isnot [byte[]]) { throw "Expected binary content from $ReportName download" } + + $JsonText = $null + $ZipStream = [System.IO.MemoryStream]::new($ZipBytes, $false) + try { + $Archive = [System.IO.Compression.ZipArchive]::new($ZipStream, [System.IO.Compression.ZipArchiveMode]::Read) + try { + $Entry = $Archive.Entries | Where-Object { $_.Name -like '*.json' } | Select-Object -First 1 + if (-not $Entry) { throw "No JSON entry in $ReportName archive" } + $EntryStream = $Entry.Open() + try { + $Reader = [System.IO.StreamReader]::new($EntryStream) + try { $JsonText = $Reader.ReadToEnd() } finally { $Reader.Dispose() } + } finally { $EntryStream.Dispose() } + } finally { $Archive.Dispose() } + } finally { + $ZipStream.Dispose() + $ZipBytes = $null } - if ($DeviceRequests) { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Fetching devices for $($DetectedApps.Count) detected apps" -sev Debug - $DeviceResults = New-GraphBulkRequest -Requests @($DeviceRequests) -tenantid $TenantFilter - - # Add devices to each detected app object - $DetectedAppsWithDevices = foreach ($App in $DetectedApps) { - $Devices = Get-GraphBulkResultByID -Results $DeviceResults -ID $App.id -Value - $App | Add-Member -NotePropertyName 'managedDevices' -NotePropertyValue ($Devices ?? @()) -Force - $App + $ExportRows = @(($JsonText | ConvertFrom-Json).values) + $JsonText = $null + + $AppsByKey = @{} + foreach ($Row in $ExportRows) { + $AppId = $Row.ApplicationKey + if (-not $AppId) { continue } + if (-not $AppsByKey.ContainsKey($AppId)) { + $AppsByKey[$AppId] = [pscustomobject]@{ + id = $AppId + displayName = $Row.ApplicationName + version = $Row.ApplicationVersion + publisher = $Row.ApplicationPublisher + platform = $Row.Platform + deviceCount = 0 + managedDevices = [System.Collections.Generic.List[object]]::new() + } } - - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DetectedApps' -Data $DetectedAppsWithDevices -AddCount - $DetectedApps = $null - $DetectedAppsWithDevices = $null - } else { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DetectedApps' -Data $DetectedApps -AddCount - $DetectedApps = $null + $App = $AppsByKey[$AppId] + $App.managedDevices.Add([pscustomobject]@{ + id = $Row.DeviceId + deviceName = $Row.DeviceName + osVersion = $Row.OSVersion + platform = $Row.Platform + userId = $Row.UserId + userPrincipalName = $Row.UserName + emailAddress = $Row.EmailAddress + }) + $App.deviceCount++ } - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached detected apps with devices successfully' -sev Debug + $DetectedApps = @($AppsByKey.Values) + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DetectedApps' -Data $DetectedApps -AddCount + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($DetectedApps.Count) detected apps with devices from export $JobId" -sev Info + Remove-AzDataTableEntity @JobsTable -Entity $JobRow -Force -ErrorAction SilentlyContinue } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter ` - -message "Failed to cache detected apps: $($_.Exception.Message)" -sev Error + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache detected apps: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage } } From b79c296f0344bdd0783568896ced47bbce8380d4 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 9 Jun 2026 13:57:18 +0200 Subject: [PATCH 175/202] sherweb migration fixes --- .../Push-ExecScheduledCommand.ps1 | 2 +- .../CIPPCore/Public/Add-CIPPScheduledTask.ps1 | 2 +- .../Register-CippExtensionScheduledTasks.ps1 | 36 ++++++- .../Sherweb/Invoke-SherwebMigration.ps1 | 98 +++++++++++++++++++ .../Sherweb/Test-SherwebMigrationAccounts.ps1 | 92 ----------------- 5 files changed, 134 insertions(+), 96 deletions(-) create mode 100644 Modules/CippExtensions/Public/Sherweb/Invoke-SherwebMigration.ps1 delete mode 100644 Modules/CippExtensions/Public/Sherweb/Test-SherwebMigrationAccounts.ps1 diff --git a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 index fde3ca8e0658..505570ec1ae5 100644 --- a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 +++ b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 @@ -179,7 +179,7 @@ function Push-ExecScheduledCommand { return } - if ($Command.Module -notin @('CIPPCore', 'CIPPAlerts', 'CIPPStandards', 'CIPPTests', 'CIPPDB')) { + if ($Command.Module -notin @('CIPPCore', 'CIPPAlerts', 'CIPPStandards', 'CIPPTests', 'CIPPDB', 'CippExtensions')) { $State = 'Failed' Write-LogMessage -headers $Headers -API 'ScheduledTask' -message "Blocked attempt to schedule command from unauthorized module: $($Command.ModuleName)\$($Item.Command)" -Sev 'Warning' $Results = "Task blocked: The command '$($Item.Command)' is not permitted to run as a scheduled task." diff --git a/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 b/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 index 94142b32371c..1a3898b33ffd 100644 --- a/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 @@ -91,7 +91,7 @@ function Add-CIPPScheduledTask { return "Error - The command '$RequestedCommand' does not exist and cannot be scheduled." } - if ($Command.Module -notin @('CIPPCore', 'CIPPAlerts', 'CIPPStandards', 'CIPPTests', 'CIPPDB')) { + if ($Command.Module -notin @('CIPPCore', 'CIPPAlerts', 'CIPPStandards', 'CIPPTests', 'CIPPDB', 'CippExtensions')) { Write-LogMessage -headers $Headers -API 'ScheduledTask' -message "Blocked attempt to schedule command from unauthorized module: $($Command.ModuleName)\$RequestedCommand" -Sev 'Warning' return "Error - The command '$RequestedCommand' is not permitted to run as a scheduled task." } diff --git a/Modules/CippExtensions/Public/Extension Functions/Register-CippExtensionScheduledTasks.ps1 b/Modules/CippExtensions/Public/Extension Functions/Register-CippExtensionScheduledTasks.ps1 index 1cef75d498fe..402f8146bdca 100644 --- a/Modules/CippExtensions/Public/Extension Functions/Register-CippExtensionScheduledTasks.ps1 +++ b/Modules/CippExtensions/Public/Extension Functions/Register-CippExtensionScheduledTasks.ps1 @@ -2,7 +2,7 @@ function Register-CIPPExtensionScheduledTasks { param( [switch]$Reschedule, [int64]$NextSync = (([datetime]::UtcNow.AddMinutes(30)) - (Get-Date '1/1/1970')).TotalSeconds, - [string[]]$Extensions = @('Hudu', 'NinjaOne', 'CustomData') + [string[]]$Extensions = @('Hudu', 'NinjaOne', 'CustomData', 'Sherweb') ) # get extension configuration and mappings table @@ -29,6 +29,38 @@ function Register-CIPPExtensionScheduledTasks { foreach ($Extension in $Extensions) { $ExtensionConfig = $Config.$Extension if ($ExtensionConfig.Enabled -eq $true -or $Extension -eq 'CustomData') { + if ($Extension -eq 'Sherweb') { + # Sherweb migration tasks - schedule per mapped tenant + $SherwebMappings = Get-CIPPAzDataTableEntity @MappingsTable -Filter "PartitionKey eq 'SherwebMapping'" + $SherwebMigTasks = Get-CIPPAzDataTableEntity @ScheduledTasksTable -Filter 'Hidden eq true' | Where-Object { $_.Command -match 'Invoke-SherwebMigration' } + foreach ($Mapping in $SherwebMappings) { + $Tenant = $Tenants | Where-Object { $_.customerId -eq $Mapping.RowKey } + if (-not $Tenant) { continue } + $MappedTenants.Add($Tenant.defaultDomainName) + $ExistingMigTask = $SherwebMigTasks | Where-Object { $_.Tenant -eq $Tenant.defaultDomainName } + if (-not $ExistingMigTask -or $Reschedule.IsPresent) { + $Task = [pscustomobject]@{ + Name = 'Sherweb Migration Check' + Command = @{ + value = 'Invoke-SherwebMigration' + label = 'Invoke-SherwebMigration' + } + Parameters = [pscustomobject]@{ + TenantFilter = $Tenant.defaultDomainName + } + Recurrence = '1d' + ScheduledTime = $NextSync + TenantFilter = $Tenant.defaultDomainName + } + if ($ExistingMigTask) { + $Task | Add-Member -NotePropertyName 'RowKey' -NotePropertyValue $ExistingMigTask.RowKey -Force + } + $null = Add-CIPPScheduledTask -Task $Task -hidden $true -SyncType 'Sherweb' + Write-Information "Creating Sherweb migration task for tenant $($Tenant.defaultDomainName)" + } + } + continue + } if ($Extension -eq 'CustomData') { $CustomDataMappingTable = Get-CIPPTable -TableName CustomDataMappings $Mappings = Get-CIPPAzDataTableEntity @CustomDataMappingTable | ForEach-Object { @@ -77,7 +109,7 @@ function Register-CIPPExtensionScheduledTasks { continue } $MappedTenants.Add($Tenant.defaultDomainName) - + # Legacy Sync-CippExtensionData tasks are no longer needed - extensions now use CippReportingDB # All cache data is now collected by Push-CIPPDBCacheData scheduled tasks diff --git a/Modules/CippExtensions/Public/Sherweb/Invoke-SherwebMigration.ps1 b/Modules/CippExtensions/Public/Sherweb/Invoke-SherwebMigration.ps1 new file mode 100644 index 000000000000..be26e9d260c7 --- /dev/null +++ b/Modules/CippExtensions/Public/Sherweb/Invoke-SherwebMigration.ps1 @@ -0,0 +1,98 @@ +function Invoke-SherwebMigration { + [CmdletBinding()] + param ( + $TenantFilter + ) + + $Table = Get-CIPPTable -TableName Extensionsconfig + $ExtensionConfig = (Get-CIPPAzDataTableEntity @Table).config | ConvertFrom-Json + $Config = $ExtensionConfig.Sherweb + + # Get licenses within the transfer window (renewing within 7 days) + $Licenses = Get-CIPPLicenseOverview -TenantFilter $TenantFilter | Where-Object { + $null -ne $_.TermInfo -and ($_.TermInfo | Where-Object { $_.DaysUntilRenew -le 7 -and $_.DaysUntilRenew -ge 0 }) + } + + if (-not $Licenses) { return } + + # Check if the exact count of licenses is available at Sherweb, if not, we need to migrate them. + $SherwebLicenses = Get-SherwebCurrentSubscription -TenantFilter $TenantFilter + $LicencesToMigrate = foreach ($License in $Licenses) { + foreach ($Term in $License.TermInfo) { + if ($Term.DaysUntilRenew -gt 7 -or $Term.DaysUntilRenew -lt 0) { continue } + $matchedSherweb = $SherwebLicenses | Where-Object { $_.quantity -eq $Term.TotalLicenses -and $_.commitmentTerm.termEndDate -eq $Term.NextLifecycle } + if (-not $matchedSherweb) { + [PSCustomObject]@{ + LicenseName = $License.License + SkuId = $License.skuId + SubscriptionId = $Term.SubscriptionId + Term = $Term.Term + NextLifecycle = $Term.NextLifecycle + DaysUntilRenew = $Term.DaysUntilRenew + TotalLicensesAtUnknownCSP = $Term.TotalLicenses + TotalLicensesAvailableInM365 = $License.TotalLicenses + } + } + } + } + + if (-not $LicencesToMigrate) { return } + + switch -wildcard ($Config.migrationMethods) { + '*notify*' { + $Subject = "Sherweb Migration: $($TenantFilter) - $($LicencesToMigrate.Count) licenses to migrate" + $HTMLContent = New-CIPPAlertTemplate -Data $LicencesToMigrate -Format 'html' -InputObject 'sherwebmig' + $JSONContent = New-CIPPAlertTemplate -Data $LicencesToMigrate -Format 'json' -InputObject 'sherwebmig' + Send-CIPPAlert -Type 'email' -Title $Subject -HTMLContent $HTMLContent.htmlcontent -TenantFilter $TenantFilter -APIName 'Alerts' + Send-CIPPAlert -Type 'psa' -Title $Subject -HTMLContent $HTMLContent.htmlcontent -TenantFilter $TenantFilter -APIName 'Alerts' + Send-CIPPAlert -Type 'webhook' -JSONContent $JSONContent -TenantFilter $TenantFilter -APIName 'Alerts' + } + '*buy*' { + try { + foreach ($MigLicense in $LicencesToMigrate) { + $PotentialLicense = Get-SherwebCatalog -TenantFilter $TenantFilter | Where-Object { $_.microsoftSkuId -eq $MigLicense.SkuId -and $_.sku -like "*$($Config.migrateToLicense)" } | Select-Object -First 1 + if (-not $PotentialLicense) { + throw "Cannot buy new license: no matching license found in catalog for SKU $($MigLicense.SkuId)" + } + Set-SherwebSubscription -TenantFilter $TenantFilter -SKU $PotentialLicense.sku -Quantity $MigLicense.TotalLicensesAtUnknownCSP + } + } catch { + $Subject = "Sherweb Migration: $($TenantFilter) - Failed to buy licenses." + $HTMLContent = New-CIPPAlertTemplate -Data $LicencesToMigrate -Format 'html' -InputObject 'sherwebmigBuyFail' + $JSONContent = New-CIPPAlertTemplate -Data $LicencesToMigrate -Format 'json' -InputObject 'sherwebmigBuyFail' + Send-CIPPAlert -Type 'email' -Title $Subject -HTMLContent $HTMLContent.htmlcontent -TenantFilter $TenantFilter -APIName 'Alerts' + Send-CIPPAlert -Type 'psa' -Title $Subject -HTMLContent $HTMLContent.htmlcontent -TenantFilter $TenantFilter -APIName 'Alerts' + Send-CIPPAlert -Type 'webhook' -JSONContent $JSONContent -TenantFilter $TenantFilter -APIName 'Alerts' + } + } + '*cancel*' { + try { + $TenantId = (Get-Tenants -TenantFilter $TenantFilter).customerId + $Pax8Config = $ExtensionConfig.Pax8 + $Pax8ClientId = $Pax8Config.clientId + $Pax8ClientSecret = Get-ExtensionAPIKey -Extension 'Pax8' + $paxBody = @{ + client_id = $Pax8ClientId + client_secret = $Pax8ClientSecret + audience = 'https://api.pax8.com' + grant_type = 'client_credentials' + } + $Token = Invoke-RestMethod -Uri 'https://api.pax8.com/v1/token' -Method POST -Body $paxBody -ContentType 'application/x-www-form-urlencoded' + $Pax8Headers = @{ Authorization = "Bearer $($Token.access_token)" } + $cancelSubList = (Invoke-RestMethod -Uri "https://api.pax8.com/v1/subscriptions?page=0&size=100&status=Active&companyId=$($TenantId)" -Method GET -Headers $Pax8Headers).content | Where-Object { $_.productId -in $LicencesToMigrate.SkuId } + foreach ($Sub in $cancelSubList) { + #Cancelbody can be NULL, or a date in the format of 2000-10-31T01:30:00.000-05:00. This used to just be $null + $cancelBody = @{ cancellationDate = (Get-Date).ToString('yyyy-MM-ddTHH:mm:ss.fffzzz') } + $null = Invoke-RestMethod -Uri "https://api.pax8.com/v1/subscriptions/$($Sub.id)" -Method DELETE -Headers $Pax8Headers -ContentType 'application/json' -Body ($cancelBody | ConvertTo-Json) + } + } catch { + $Subject = "Sherweb Migration: $($TenantFilter) - Pax8 cancellation failed." + $HTMLContent = New-CIPPAlertTemplate -Data $LicencesToMigrate -Format 'html' -InputObject 'sherwebmigfailcancel' + $JSONContent = New-CIPPAlertTemplate -Data $LicencesToMigrate -Format 'json' -InputObject 'sherwebmigfailcancel' + Send-CIPPAlert -Type 'email' -Title $Subject -HTMLContent $HTMLContent.htmlcontent -TenantFilter $TenantFilter -APIName 'Alerts' + Send-CIPPAlert -Type 'psa' -Title $Subject -HTMLContent $HTMLContent.htmlcontent -TenantFilter $TenantFilter -APIName 'Alerts' + Send-CIPPAlert -Type 'webhook' -JSONContent $JSONContent -TenantFilter $TenantFilter -APIName 'Alerts' + } + } + } +} diff --git a/Modules/CippExtensions/Public/Sherweb/Test-SherwebMigrationAccounts.ps1 b/Modules/CippExtensions/Public/Sherweb/Test-SherwebMigrationAccounts.ps1 deleted file mode 100644 index 1fdc01e00176..000000000000 --- a/Modules/CippExtensions/Public/Sherweb/Test-SherwebMigrationAccounts.ps1 +++ /dev/null @@ -1,92 +0,0 @@ -function Test-SherwebMigrationAccounts { - [CmdletBinding()] - param ( - $TenantFilter - ) - - $Table = Get-CIPPTable -TableName Extensionsconfig - $ExtensionConfig = (Get-CIPPAzDataTableEntity @Table).config | ConvertFrom-Json - $Config = $ExtensionConfig.Sherweb - #First get a list of all subscribed skus for this tenant, that are in the transfer window. - $Licenses = (Get-CIPPLicenseOverview -TenantFilter $TenantFilter) | Where-Object { $null -ne $_.terminfo -and $_.terminfo.TransferWindow -le 7 } - - #now check if this exact count of licenses is available at Sherweb, if not, we need to migrate them. - $SherwebLicenses = Get-SherwebCurrentSubscription -TenantFilter $TenantFilter - $LicencesToMigrate = foreach ($License in $Licenses) { - foreach ($termInfo in $License.terminfo) { - $matchedSherweb = $SherwebLicenses | Where-Object { $_.quantity -eq $termInfo.TotalLicenses -and $_.commitmentTerm.termEndDate -eq $termInfo.NextLifecycle } - if (-not $matchedSherweb) { - [PSCustomObject]@{ - LicenseName = ($Licenses | Where-Object { $_.skuId -eq $License.skuId }).license - SkuId = $License.skuId - SubscriptionId = $termInfo.SubscriptionId - Term = $termInfo.Term - NextLifecycle = $termInfo.NextLifecycle - TotalLicensesAtUnknownCSP = $termInfo.TotalLicenses - TotalLicensesAvailableInM365 = ($Licenses | Where-Object { $_.skuId -eq $License.skuId }).TotalLicenses - } - - } - } - } - - switch -wildcard ($config.migrationMethods) { - '*notify*' { - $Subject = "Sherweb Migration: $($TenantFilter) - $($LicencesToMigrate.Count) licenses to migrate" - $HTMLContent = New-CIPPAlertTemplate -Data $LicencesToMigrate -Format 'html' -InputObject 'sherwebmig' - $JSONContent = New-CIPPAlertTemplate -Data $LicencesToMigrate -Format 'json' -InputObject 'sherwebmig' - Send-CIPPAlert -Type 'email' -Title $Subject -HTMLContent $HTMLContent.htmlcontent -TenantFilter $tenant -APIName 'Alerts' - Send-CIPPAlert -Type 'psa' -Title $Subject -HTMLContent $HTMLContent.htmlcontent -TenantFilter $standardsTenant -APIName 'Alerts' - Send-CIPPAlert -Type 'webhook' -JSONContent $JSONContent -TenantFilter $Tenant -APIName 'Alerts' - } - '*buy*' { - try { - $PotentialLicenses = Get-SherwebCatalog -TenantFilter $TenantFilter | Where-Object { $_.microsoftSkuId -in $LicencesToMigrate.SkuId -and $_.sku -like "*$($Config.migrateToLicense)" } - if (!$PotentialLicenses) { - throw 'cannot buy new license: no matching license found in catalog' - } else { - $PotentialLicenses | ForEach-Object { - Set-SherwebSubscription -TenantFilter $TenantFilter -SKU $PotentialLicenses.sku -Quantity $LicencesToMigrate.TotalLicensesAtUnknownCSP - } - } - } catch { - $Subject = "Sherweb Migration: $($TenantFilter) - Failed to buy licenses." - $HTMLContent = New-CIPPAlertTemplate -Data $LicencesToMigrate -Format 'html' -InputObject 'sherwebmigBuyFail' - $JSONContent = New-CIPPAlertTemplate -Data $LicencesToMigrate -Format 'json' -InputObject 'sherwebmigBuyFail' - Send-CIPPAlert -Type 'email' -Title $Subject -HTMLContent $HTMLContent.htmlcontent -TenantFilter $tenant -APIName 'Alerts' - Send-CIPPAlert -Type 'psa' -Title $Subject -HTMLContent $HTMLContent.htmlcontent -TenantFilter $standardsTenant -APIName 'Alerts' - Send-CIPPAlert -Type 'webhook' -JSONContent $JSONContent -TenantFilter $Tenant -APIName 'Alerts' - } - - } - '*Cancel' { - try { - $tenantid = (Get-Tenants -TenantFilter $TenantFilter).customerId - $Pax8Config = $ExtensionConfig.Pax8 - $Pax8ClientId = $Pax8Config.clientId - $Pax8ClientSecret = Get-ExtensionAPIKey -Extension 'Pax8' - $paxBody = @{ - client_id = $Pax8ClientId - client_secret = $Pax8ClientSecret - audience = 'https://api.pax8.com' - grant_type = 'client_credentials' - } - $Token = Invoke-RestMethod -Uri 'https://api.pax8.com/v1/token' -Method POST -Headers $headers -ContentType 'application/json' -Body $paxBody - $headers = @{ Authorization = "Bearer $($Token.access_token)" } - $cancelSubList = Invoke-RestMethod -Uri "https://api.pax8.com/v1/subscriptions?page=0&size=10&status=Active&companyId=$($tenantid)" -Method GET -Headers $headers | Where-Object -Property productId -In $LicencesToMigrate.SkuId - $cancelSubList | ForEach-Object { - $response = Invoke-RestMethod -Uri "https://api.pax8.com/v1/subscriptions/$($_.subscriptionId)" -Method DELETE -Headers $headers -ContentType 'application/json' -Body ($body | ConvertTo-Json) - } - - } catch { - $Subject = 'Sherweb Migration: Pax Migration failed' - $HTMLContent = New-CIPPAlertTemplate -Data $LicencesToMigrate -Format 'html' -InputObject 'sherwebmigfailpax' - $JSONContent = New-CIPPAlertTemplate -Data $LicencesToMigrate -Format 'json' -InputObject 'sherwebmigfailpax' - Send-CIPPAlert -Type 'email' -Title $Subject -HTMLContent $HTMLContent.htmlcontent -TenantFilter $tenant -APIName 'Alerts' - Send-CIPPAlert -Type 'psa' -Title $Subject -HTMLContent $HTMLContent.htmlcontent -TenantFilter $standardsTenant -APIName 'Alerts' - Send-CIPPAlert -Type 'webhook' -JSONContent $JSONContent -TenantFilter $Tenant -APIName 'Alerts' - } - } - - } -} From 280b3b9d08578311a60d3baf501984b205356fb6 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 9 Jun 2026 07:57:29 -0400 Subject: [PATCH 176/202] fix: add CippExtensions to allowlist --- .../Activity Triggers/Push-ExecScheduledCommand.ps1 | 2 +- Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 | 4 ++-- profile.ps1 | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 index fde3ca8e0658..505570ec1ae5 100644 --- a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 +++ b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 @@ -179,7 +179,7 @@ function Push-ExecScheduledCommand { return } - if ($Command.Module -notin @('CIPPCore', 'CIPPAlerts', 'CIPPStandards', 'CIPPTests', 'CIPPDB')) { + if ($Command.Module -notin @('CIPPCore', 'CIPPAlerts', 'CIPPStandards', 'CIPPTests', 'CIPPDB', 'CippExtensions')) { $State = 'Failed' Write-LogMessage -headers $Headers -API 'ScheduledTask' -message "Blocked attempt to schedule command from unauthorized module: $($Command.ModuleName)\$($Item.Command)" -Sev 'Warning' $Results = "Task blocked: The command '$($Item.Command)' is not permitted to run as a scheduled task." diff --git a/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 b/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 index 94142b32371c..576c2bcca127 100644 --- a/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 @@ -70,7 +70,7 @@ function Add-CIPPScheduledTask { $ImportedModules = [System.Collections.Generic.List[string]]::new() if (-not $Command) { try { - foreach ($SiblingModule in @('CIPPStandards', 'CIPPAlerts', 'CIPPTests', 'CIPPDB')) { + foreach ($SiblingModule in @('CIPPStandards', 'CIPPAlerts', 'CIPPTests', 'CIPPDB', 'CippExtensions')) { if (-not (Get-Module -Name $SiblingModule)) { Import-Module $SiblingModule -ErrorAction SilentlyContinue if (Get-Module -Name $SiblingModule) { @@ -91,7 +91,7 @@ function Add-CIPPScheduledTask { return "Error - The command '$RequestedCommand' does not exist and cannot be scheduled." } - if ($Command.Module -notin @('CIPPCore', 'CIPPAlerts', 'CIPPStandards', 'CIPPTests', 'CIPPDB')) { + if ($Command.Module -notin @('CIPPCore', 'CIPPAlerts', 'CIPPStandards', 'CIPPTests', 'CIPPDB', 'CippExtensions')) { Write-LogMessage -headers $Headers -API 'ScheduledTask' -message "Blocked attempt to schedule command from unauthorized module: $($Command.ModuleName)\$RequestedCommand" -Sev 'Warning' return "Error - The command '$RequestedCommand' is not permitted to run as a scheduled task." } diff --git a/profile.ps1 b/profile.ps1 index a93372c3085c..a9041f6e9cc6 100644 --- a/profile.ps1 +++ b/profile.ps1 @@ -216,7 +216,7 @@ $Timings['Timezone'] = $SwTimezone.Elapsed.TotalMilliseconds # Import Extra modules if needed $SwExtraModules = [System.Diagnostics.Stopwatch]::StartNew() $ModulesPath = Join-Path $env:CIPPRootPath 'Modules' -$NonHttpModules = @('CIPPStandards', 'CIPPAlerts', 'CIPPTests', 'CIPPDB', 'CIPPActivityTriggers', 'DNSHealth') +$NonHttpModules = @('CIPPStandards', 'CIPPAlerts', 'CIPPTests', 'CIPPDB', 'CIPPActivityTriggers', 'DNSHealth', 'CippExtensions') $HttpModule = @('CIPPHTTP') $HttpDisabled = $env:AzureWebJobs_CIPPHttpTrigger_Disabled -in @('true', '1') -or [System.Environment]::GetEnvironmentVariable('AzureWebJobs.CIPPHttpTrigger.Disabled') -in @('true', '1') From 2511c6e56640e03ed49000f7133cc027fcea1717 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 9 Jun 2026 14:50:17 +0200 Subject: [PATCH 177/202] add limits for tools copilot studio --- .../Public/MCP/Get-CippMcpToolList.ps1 | 171 +++++++++++------- .../CIPP/MCP/Invoke-ExecMcp.ps1 | 2 +- 2 files changed, 109 insertions(+), 64 deletions(-) diff --git a/Modules/CIPPCore/Public/MCP/Get-CippMcpToolList.ps1 b/Modules/CIPPCore/Public/MCP/Get-CippMcpToolList.ps1 index a102a82e340b..5852d6d966e8 100644 --- a/Modules/CIPPCore/Public/MCP/Get-CippMcpToolList.ps1 +++ b/Modules/CIPPCore/Public/MCP/Get-CippMcpToolList.ps1 @@ -1,89 +1,134 @@ function Get-CippMcpToolList { <# .SYNOPSIS - Projects the CIPP OpenAPI spec into the read-only MCP tool list. + Projects the CIPP OpenAPI spec into the read-only MCP tool list, with optional per-connection filtering. .DESCRIPTION Returns every operation whose x-cipp-role ends in '.Read' (never '.ReadWrite') as an MCP tool definition: name (the API endpoint), description, inputSchema (JSON Schema built from the operation's query parameters / request body with $ref inlined), and - read-only annotations. Cached per worker; pass -Force to rebuild. Not an entrypoint. - The spec is consumed as nested hashtables (Get-CippMcpSpec uses -AsHashtable). + read-only annotations. The full projection is cached per worker; pass -Force to rebuild. + + The connector URL's query string filters what is advertised (the query is NOT part of the + OAuth resource, so it does not affect auth). One CIPP instance can therefore back several + connector instances, each scoped to a subset. Supported query parameters: + ?tags=Identity,Exchange only tools in those top-level CIPP categories (the OpenAPI tag) + ?tools=ListUsers,ListGroups explicit allow-list of tool names + ?first=70 / ?limit=70 cap the number of tools (e.g. for clients with a tool ceiling) .FUNCTIONALITY Internal #> [CmdletBinding()] - param([switch]$Force) - - if ($script:CippMcpToolListCache -and -not $Force) { - return $script:CippMcpToolListCache - } + param( + $Request, + [switch]$Force + ) + + # Build (and cache) the full read-only tool list. Each cached entry carries an internal _category + # (top-level OpenAPI tag) used only for filtering; it is stripped before the list is returned. + if (-not $script:CippMcpToolListCache -or $Force) { + $Spec = Get-CippMcpSpec + $Tools = [System.Collections.Generic.List[object]]::new() + + foreach ($PathEntry in $Spec['paths'].GetEnumerator()) { + $Endpoint = $PathEntry.Key -replace '^/api/', '' + + # Never expose the MCP transport itself as a tool. + if ($Endpoint -eq 'ExecMcp') { continue } + + foreach ($MethodEntry in $PathEntry.Value.GetEnumerator()) { + $Method = [string]$MethodEntry.Key + if ($Method -notin @('get', 'post')) { continue } + + $Op = $MethodEntry.Value + $Role = $Op['x-cipp-role'] + + # Read-only surface only. + if (-not $Role -or $Role -notmatch '\.Read$') { continue } + + # Defensive backstop: never expose an endpoint whose name implies a mutation, + # even if its x-cipp-role is mislabeled '.Read' (e.g. AddTestReport, EditIntunePolicy). + if ($Endpoint -match '^(Add|Set|Remove|Delete|Edit|New|Update|Disable|Enable|Reset|Revoke|Push|Clear|Start|Stop|Rename|Move|Copy)') { continue } + + $Properties = [ordered]@{} + $RequiredList = [System.Collections.Generic.List[string]]::new() + + # Query / path parameters. + foreach ($ParamRaw in @($Op['parameters'])) { + if (-not $ParamRaw) { continue } + $Param = Resolve-CippMcpNode -Node $ParamRaw -Spec $Spec + if ($Param['in'] -notin @('query', 'path')) { continue } + $Schema = if ($Param['schema']) { $Param['schema'] } else { @{ type = 'string' } } + $Properties[[string]$Param['name']] = $Schema + if ($Param['required']) { $RequiredList.Add([string]$Param['name']) } + } - $Spec = Get-CippMcpSpec - $Tools = [System.Collections.Generic.List[object]]::new() + # Request body (uncommon for reads; included for completeness). + if ($Op['requestBody'] -and $Op['requestBody']['content'] -and $Op['requestBody']['content']['application/json']) { + $BodySchema = Resolve-CippMcpNode -Node $Op['requestBody']['content']['application/json']['schema'] -Spec $Spec + if ($BodySchema -and $BodySchema['properties']) { + foreach ($BodyProp in $BodySchema['properties'].GetEnumerator()) { + $Properties[[string]$BodyProp.Key] = $BodyProp.Value + } + foreach ($Req in @($BodySchema['required'])) { if ($Req) { $RequiredList.Add([string]$Req) } } + } + } - foreach ($PathEntry in $Spec['paths'].GetEnumerator()) { - $Endpoint = $PathEntry.Key -replace '^/api/', '' + $InputSchema = [ordered]@{ + type = 'object' + properties = $Properties + } + if ($RequiredList.Count -gt 0) { + $InputSchema['required'] = @($RequiredList | Select-Object -Unique) + } - # Never expose the MCP transport itself as a tool. - if ($Endpoint -eq 'ExecMcp') { continue } + $Tag = @($Op['tags'])[0] + $Category = if ($Tag) { ([string]$Tag -split '\s*>\s*')[0].Trim() } else { 'Uncategorized' } - foreach ($MethodEntry in $PathEntry.Value.GetEnumerator()) { - $Method = [string]$MethodEntry.Key - if ($Method -notin @('get', 'post')) { continue } + $Tools.Add([ordered]@{ + name = $Endpoint + description = Get-CippMcpDescription -Operation $Op + inputSchema = $InputSchema + annotations = [ordered]@{ title = $Endpoint; readOnlyHint = $true } + _category = $Category + }) + } + } - $Op = $MethodEntry.Value - $Role = $Op['x-cipp-role'] + $script:CippMcpToolListCache = $Tools + } - # Read-only surface only. - if (-not $Role -or $Role -notmatch '\.Read$') { continue } + $Filtered = @($script:CippMcpToolListCache) - # Defensive backstop: never expose an endpoint whose name implies a mutation, - # even if its x-cipp-role is mislabeled '.Read' (e.g. AddTestReport, EditIntunePolicy). - if ($Endpoint -match '^(Add|Set|Remove|Delete|Edit|New|Update|Disable|Enable|Reset|Revoke|Push|Clear|Start|Stop|Rename|Move|Copy)') { continue } + # Per-connection filtering from the connector URL's query string. + $Query = $Request.Query + $TagFilter = "$($Query.tags ?? $Query.category ?? $Query.tag)".Trim() + if ($TagFilter) { + $WantedCats = @($TagFilter -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) + $Filtered = @($Filtered | Where-Object { $_._category -in $WantedCats }) + } - $Properties = [ordered]@{} - $RequiredList = [System.Collections.Generic.List[string]]::new() + $ToolFilter = "$($Query.tools)".Trim() + if ($ToolFilter) { + $WantedTools = @($ToolFilter -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) + $Filtered = @($Filtered | Where-Object { $_.name -in $WantedTools }) + } - # Query / path parameters. - foreach ($ParamRaw in @($Op['parameters'])) { - if (-not $ParamRaw) { continue } - $Param = Resolve-CippMcpNode -Node $ParamRaw -Spec $Spec - if ($Param['in'] -notin @('query', 'path')) { continue } - $Schema = if ($Param['schema']) { $Param['schema'] } else { @{ type = 'string' } } - $Properties[[string]$Param['name']] = $Schema - if ($Param['required']) { $RequiredList.Add([string]$Param['name']) } - } + $Limit = ($Query.first ?? $Query.limit) -as [int] + if ($Limit -gt 0) { + $Filtered = @($Filtered | Select-Object -First $Limit) + } - # Request body (uncommon for reads; included for completeness). - if ($Op['requestBody'] -and $Op['requestBody']['content'] -and $Op['requestBody']['content']['application/json']) { - $BodySchema = Resolve-CippMcpNode -Node $Op['requestBody']['content']['application/json']['schema'] -Spec $Spec - if ($BodySchema -and $BodySchema['properties']) { - foreach ($BodyProp in $BodySchema['properties'].GetEnumerator()) { - $Properties[[string]$BodyProp.Key] = $BodyProp.Value - } - foreach ($Req in @($BodySchema['required'])) { if ($Req) { $RequiredList.Add([string]$Req) } } - } - } + Write-Information "[MCP] tools/list -> $($Filtered.Count) tools (tags='$TagFilter' tools='$ToolFilter' first='$Limit')" - $InputSchema = [ordered]@{ - type = 'object' - properties = $Properties + # Project to the wire shape (drop the internal _category). + return @($Filtered | ForEach-Object { + [ordered]@{ + name = $_.name + description = $_.description + inputSchema = $_.inputSchema + annotations = $_.annotations } - if ($RequiredList.Count -gt 0) { - $InputSchema['required'] = @($RequiredList | Select-Object -Unique) - } - - $Tools.Add([ordered]@{ - name = $Endpoint - description = Get-CippMcpDescription -Operation $Op - inputSchema = $InputSchema - annotations = [ordered]@{ title = $Endpoint; readOnlyHint = $true } - }) - } - } - - $script:CippMcpToolListCache = $Tools - return $Tools + }) } function Resolve-CippMcpNode { diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/MCP/Invoke-ExecMcp.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/MCP/Invoke-ExecMcp.ps1 index 60da65909eba..f2d9b427868a 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/MCP/Invoke-ExecMcp.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/MCP/Invoke-ExecMcp.ps1 @@ -63,7 +63,7 @@ function Invoke-ExecMcp { } } 'ping' { $Result = @{} } - 'tools/list' { $Result = [ordered]@{ tools = @(Get-CippMcpToolList) } } + 'tools/list' { $Result = [ordered]@{ tools = @(Get-CippMcpToolList -Request $Request) } } 'tools/call' { $Result = Get-CippMcpToolResult -Request $Request -TriggerMetadata $TriggerMetadata -ToolName $Rpc.params.name -Arguments $Rpc.params.arguments } From 6c897f52f26b6a3a75c5a042b18e140f8e44d13c Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 9 Jun 2026 21:17:48 +0800 Subject: [PATCH 178/202] fixes group addition in user page and vacation mode --- .../CIPPCore/Public/Add-CIPPGroupMember.ps1 | 70 ++++++------------- Modules/CIPPCore/Public/New-CIPPUserTask.ps1 | 2 +- 2 files changed, 24 insertions(+), 48 deletions(-) diff --git a/Modules/CIPPCore/Public/Add-CIPPGroupMember.ps1 b/Modules/CIPPCore/Public/Add-CIPPGroupMember.ps1 index 547634031dfc..3a5ea194fe18 100644 --- a/Modules/CIPPCore/Public/Add-CIPPGroupMember.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPGroupMember.ps1 @@ -45,54 +45,37 @@ function Add-CIPPGroupMember { } $Users = New-GraphBulkRequest -Requests @($Requests) -tenantid $TenantFilter - $SuccessfulUsers = [System.Collections.Generic.List[string]]::new() - $FailedUsers = [System.Collections.Generic.List[string]]::new() - - # Accept both human-readable labels (from Invoke-EditGroup / older callers) and - # camelCase calculatedGroupType values (from the user template / add-edit-user form) - $ExoGroupTypes = @('Distribution list', 'Distribution List', 'Mail-Enabled Security', 'distributionList', 'security') - - if ($GroupType -in $ExoGroupTypes) { + if ($GroupType -eq 'Distribution list' -or $GroupType -eq 'Mail-Enabled Security') { $ExoBulkRequests = [System.Collections.Generic.List[object]]::new() - $GuidToUpn = @{} + $ExoLogs = [System.Collections.Generic.List[object]]::new() foreach ($User in $Users) { - $UserUpn = $User.body.userPrincipalName - if (-not $UserUpn) { continue } - $OpGuid = [guid]::NewGuid().ToString() - $GuidToUpn[$OpGuid] = $UserUpn - $Params = @{ Identity = $GroupId; Member = $UserUpn; BypassSecurityGroupManagerCheck = $true } + $Params = @{ Identity = $GroupId; Member = $User.body.userPrincipalName; BypassSecurityGroupManagerCheck = $true } $ExoBulkRequests.Add(@{ - OperationGuid = $OpGuid - CmdletInput = @{ + CmdletInput = @{ CmdletName = 'Add-DistributionGroupMember' Parameters = $Params } }) + $ExoLogs.Add(@{ + message = "Added member $($User.body.userPrincipalName) to $($GroupId) group" + target = $User.body.userPrincipalName + }) } if ($ExoBulkRequests.Count -gt 0) { - $RawExoRequest = @(New-ExoBulkRequest -tenantid $TenantFilter -cmdletArray @($ExoBulkRequests)) + $RawExoRequest = New-ExoBulkRequest -tenantid $TenantFilter -cmdletArray @($ExoBulkRequests) + $LastError = $RawExoRequest | Select-Object -Last 1 - # Index responses by OperationGuid so each user is correlated by position, not by error.target - $ResponseByGuid = @{} - foreach ($Response in $RawExoRequest) { - if ($Response.OperationGuid) { - $ResponseByGuid[$Response.OperationGuid] = $Response - } + foreach ($ExoError in $LastError.error) { + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $ExoError -Sev 'Error' + throw $ExoError } - foreach ($OpGuid in $GuidToUpn.Keys) { - $UserUpn = $GuidToUpn[$OpGuid] - $Response = $ResponseByGuid[$OpGuid] - - if ($Response -and $Response.error) { - $ErrorText = if ($Response.error -is [string]) { $Response.error } else { ($Response.error | Out-String).Trim() } - Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to add member $($UserUpn) to $($GroupId): $ErrorText" -Sev 'Error' - $FailedUsers.Add($UserUpn) - } else { - Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Added member $($UserUpn) to $($GroupId) group" -Sev 'Info' - $SuccessfulUsers.Add($UserUpn) + foreach ($ExoLog in $ExoLogs) { + $ExoError = $LastError | Where-Object { $ExoLog.target -in $_.target -and $_.error } + if (!$LastError -or ($LastError.error -and $LastError.target -notcontains $ExoLog.target)) { + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $ExoLog.message -Sev 'Info' } } } @@ -108,26 +91,19 @@ function Add-CIPPGroupMember { } } $AddResults = New-GraphBulkRequest -tenantid $TenantFilter -Requests @($AddRequests) + $SuccessfulUsers = [system.collections.generic.list[string]]::new() foreach ($Result in $AddResults) { - $UserPrincipalName = $Users | Where-Object { $_.body.id -eq $Result.id } | Select-Object -ExpandProperty body | Select-Object -ExpandProperty userPrincipalName if ($Result.status -lt 200 -or $Result.status -gt 299) { - Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to add member $($UserPrincipalName): $($Result.body.error.message)" -Sev 'Error' - $FailedUsers.Add($UserPrincipalName) + $FailedUsername = $Users | Where-Object { $_.body.id -eq $Result.id } | Select-Object -ExpandProperty body | Select-Object -ExpandProperty userPrincipalName + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to add member $($FailedUsername): $($Result.body.error.message)" -Sev 'Error' } else { + $UserPrincipalName = $Users | Where-Object { $_.body.id -eq $Result.id } | Select-Object -ExpandProperty body | Select-Object -ExpandProperty userPrincipalName $SuccessfulUsers.Add($UserPrincipalName) } } } - - if ($SuccessfulUsers.Count -eq 0 -and $FailedUsers.Count -gt 0) { - $Results = "Failed to add user $($FailedUsers -join ', ') to $($GroupId)." - throw $Results - } - - $Results = "Successfully added user $($SuccessfulUsers -join ', ') to $($GroupId)." - if ($FailedUsers.Count -gt 0) { - $Results = "$Results Failed to add: $($FailedUsers -join ', ')." - } + $UserList = ($SuccessfulUsers -join ', ') + $Results = "Successfully added user $UserList to $($GroupId)." Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Results -Sev 'Info' return $Results } catch { diff --git a/Modules/CIPPCore/Public/New-CIPPUserTask.ps1 b/Modules/CIPPCore/Public/New-CIPPUserTask.ps1 index a026e122bb63..f95dfb90b586 100644 --- a/Modules/CIPPCore/Public/New-CIPPUserTask.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPUserTask.ps1 @@ -70,7 +70,7 @@ function New-CIPPUserTask { # Add to groups if ($UserObj.AddToGroups) { - $ExoGroupTypes = @('Distribution list', 'Distribution List', 'Mail-Enabled Security', 'distributionList', 'security') + $ExoGroupTypes = @('Distribution list', 'Mail-Enabled Security') $UserObj.AddToGroups | ForEach-Object { $Group = $_ $GroupType = $Group.addedFields.groupType From 39e1a34e3d33a7657f10c1d21eda71190596cdee Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 9 Jun 2026 10:35:41 -0400 Subject: [PATCH 179/202] chore: bump version to 10.5.1 --- host.json | 2 +- version_latest.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/host.json b/host.json index 5635adf20ae2..accd9b7c83bc 100644 --- a/host.json +++ b/host.json @@ -16,7 +16,7 @@ "distributedTracingEnabled": false, "version": "None" }, - "defaultVersion": "10.4.5", + "defaultVersion": "10.5.1", "versionMatchStrategy": "Strict", "versionFailureStrategy": "Fail" } diff --git a/version_latest.txt b/version_latest.txt index 2cf514e360ca..4a6e70e959e5 100644 --- a/version_latest.txt +++ b/version_latest.txt @@ -1 +1 @@ -10.5.0 +10.5.1 From 64836c02a801a3718c2bd2e598bcf206c973541d Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 9 Jun 2026 12:15:57 -0400 Subject: [PATCH 180/202] fix: rerun detection on scheduled tasks --- Modules/CIPPCore/Public/Test-CIPPRerun.ps1 | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/Modules/CIPPCore/Public/Test-CIPPRerun.ps1 b/Modules/CIPPCore/Public/Test-CIPPRerun.ps1 index 6c4dece1f8ba..f59727017896 100644 --- a/Modules/CIPPCore/Public/Test-CIPPRerun.ps1 +++ b/Modules/CIPPCore/Public/Test-CIPPRerun.ps1 @@ -63,16 +63,28 @@ function Test-CIPPRerun { if ($NewSettings.Length -ne $PreviousSettings.Length) { Write-Host "$($NewSettings.Length) vs $($PreviousSettings.Length) - settings have changed." $RerunData.EstimatedNextRun = $EstimatedNextRun + $RerunData.LastScheduledTime = "$CurrentUnixTime" $RerunData.Settings = "$($Settings | ConvertTo-Json -Depth 10 -Compress)" Add-CIPPAzDataTableEntity @RerunTable -Entity $RerunData -Force return $false # Not a rerun because settings have changed. } } + # If the task was rescheduled (ScheduledTime changed since last cache write), + # treat it as a new execution rather than a duplicate. + if ($BaseTime -gt 0 -and $RerunData.LastScheduledTime -and [int64]$RerunData.LastScheduledTime -ne $BaseTime) { + Write-Information "Task $API has a new ScheduledTime ($BaseTime vs cached $($RerunData.LastScheduledTime)). Treating as new execution." + $RerunData.EstimatedNextRun = $EstimatedNextRun + $RerunData.LastScheduledTime = "$BaseTime" + $RerunData.Settings = "$($Settings | ConvertTo-Json -Depth 10 -Compress)" + Add-CIPPAzDataTableEntity @RerunTable -Entity $RerunData -Force + return $false + } if ($RerunData.EstimatedNextRun -gt $CurrentUnixTime) { Write-LogMessage -API $API -message "$Type rerun detected for $($API). Prevented from running again." -tenant $TenantFilter -headers $Headers -Sev 'Info' return $true } else { $RerunData.EstimatedNextRun = $EstimatedNextRun + $RerunData.LastScheduledTime = "$BaseTime" $RerunData.Settings = "$($Settings | ConvertTo-Json -Depth 10 -Compress)" Add-CIPPAzDataTableEntity @RerunTable -Entity $RerunData -Force return $false @@ -80,10 +92,11 @@ function Test-CIPPRerun { } else { $EstimatedNextRun = $CurrentUnixTime + $EstimatedDifference $NewEntity = @{ - PartitionKey = "$TenantFilter" - RowKey = "$($Type)_$($API)" - Settings = "$($Settings | ConvertTo-Json -Depth 10 -Compress)" - EstimatedNextRun = $EstimatedNextRun + PartitionKey = "$TenantFilter" + RowKey = "$($Type)_$($API)" + Settings = "$($Settings | ConvertTo-Json -Depth 10 -Compress)" + EstimatedNextRun = $EstimatedNextRun + LastScheduledTime = "$CurrentUnixTime" } Add-CIPPAzDataTableEntity @RerunTable -Entity $NewEntity -Force return $false From 9dd0e56750810b7a85ef4fb079dbf6725b2b6ce0 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 10 Jun 2026 14:27:26 +0800 Subject: [PATCH 181/202] Update Invoke-ListTenantAlignment.ps1 --- .../Standards/Invoke-ListTenantAlignment.ps1 | 94 +++++++++++-------- 1 file changed, 54 insertions(+), 40 deletions(-) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListTenantAlignment.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListTenantAlignment.ps1 index b123bddf07c2..8a6b86259fcf 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListTenantAlignment.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListTenantAlignment.ps1 @@ -12,9 +12,14 @@ function Invoke-ListTenantAlignment { $APIName = $Request.Params.CIPPEndpoint $Granular = $Request.Query.granular -eq 'true' + $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter try { + $AlignmentParams = @{} + if ($TenantFilter -and $TenantFilter -ne 'AllTenants') { + $AlignmentParams.TenantFilter = $TenantFilter + } # Use the new Get-CIPPTenantAlignment function to get alignment data - $AlignmentData = Get-CIPPTenantAlignment + $AlignmentData = Get-CIPPTenantAlignment @AlignmentParams # Build a GUID -> displayName lookup from the templates table for all template types $TemplateLookup = @{} @@ -23,12 +28,13 @@ function Invoke-ListTenantAlignment { $TemplatePartitions = @('IntuneTemplate', 'ConditionalAccessTemplate', 'QuarantineTemplate') foreach ($Partition in $TemplatePartitions) { Get-CIPPAzDataTableEntity @TemplateTable -Filter "PartitionKey eq '$Partition'" | ForEach-Object { + $TemplateRow = $_ try { - $Parsed = $_.JSON | ConvertFrom-Json -ErrorAction Stop - $DisplayName = $Parsed.displayName ?? $Parsed.Displayname ?? $Parsed.DisplayName ?? $Parsed.name ?? $_.RowKey - $TemplateLookup[$_.RowKey] = $DisplayName + $Parsed = $TemplateRow.JSON | ConvertFrom-Json -ErrorAction Stop + $DisplayName = $Parsed.displayName ?? $Parsed.Displayname ?? $Parsed.DisplayName ?? $Parsed.name ?? $TemplateRow.RowKey + $TemplateLookup[$TemplateRow.RowKey] = $DisplayName } catch { - $TemplateLookup[$_.RowKey] = $_.RowKey + $TemplateLookup[$TemplateRow.RowKey] = $TemplateRow.RowKey } } } @@ -42,44 +48,51 @@ function Invoke-ListTenantAlignment { $TemplateId = $Row.StandardId $StandardType = $Row.standardType ? $Row.standardType : 'Classic Standard' $Row.ComparisonDetails | ForEach-Object { - $StandardId = $_.StandardName - $FriendlyType = $StandardType - $ResolvedName = if ($StandardId -match '^standards\.(\w+Template)\.(.+)$') { - $LookupKey = if ($Matches[1] -eq 'QuarantineTemplate') { - $KeyBytes = [byte[]]::new($Matches[2].Length / 2) - for ($i = 0; $i -lt $KeyBytes.Length; $i++) { - $KeyBytes[$i] = [Convert]::ToByte($Matches[2].Substring($i * 2, 2), 16) + $Detail = $_ + try { + $StandardId = $Detail.StandardName + $FriendlyType = $StandardType + $ResolvedName = if ($StandardId -and $StandardId -match '^standards\.(\w+Template)\.(.+)$') { + $MatchType = $Matches[1] + $MatchValue = $Matches[2] + $LookupKey = if ($MatchType -eq 'QuarantineTemplate') { + $KeyBytes = [byte[]]::new($MatchValue.Length / 2) + for ($i = 0; $i -lt $KeyBytes.Length; $i++) { + $KeyBytes[$i] = [Convert]::ToByte($MatchValue.Substring($i * 2, 2), 16) + } + [System.Text.Encoding]::UTF8.GetString($KeyBytes) + } else { + $MatchValue + } + $PolicyName = $TemplateLookup[$LookupKey] ?? $LookupKey + $FriendlyType = switch ($MatchType) { + 'IntuneTemplate' { 'Intune Template' } + 'ConditionalAccessTemplate' { 'Conditional Access Template' } + 'QuarantineTemplate' { 'Quarantine Template' } + default { $MatchType } } - [System.Text.Encoding]::UTF8.GetString($KeyBytes) + "$FriendlyType - $PolicyName" } else { - $Matches[2] + $StandardId } - $PolicyName = $TemplateLookup[$LookupKey] ?? $LookupKey - $FriendlyType = switch ($Matches[1]) { - 'IntuneTemplate' { 'Intune Template' } - 'ConditionalAccessTemplate' { 'Conditional Access Template' } - 'QuarantineTemplate' { 'Quarantine Template' } - default { $Matches[1] } + [PSCustomObject]@{ + tenantFilter = $Row.TenantFilter + templateName = $TemplateName + templateId = $TemplateId + templateType = $Row.standardType + standardType = $FriendlyType + standardId = $StandardId + standardName = $ResolvedName + complianceStatus = $Detail.ComplianceStatus + compliant = $Detail.Compliant + deviationStatus = $Detail.DeviationStatus + licenseAvailable = $Detail.LicenseAvailable + currentValue = $Detail.CurrentValue + expectedValue = $Detail.ExpectedValue + latestDataCollection = $Row.LatestDataCollection } - "$FriendlyType - $PolicyName" - } else { - $StandardId - } - [PSCustomObject]@{ - tenantFilter = $Row.TenantFilter - templateName = $TemplateName - templateId = $TemplateId - templateType = $Row.standardType - standardType = $FriendlyType - standardId = $StandardId - standardName = $ResolvedName - complianceStatus = $_.ComplianceStatus - compliant = $_.Compliant - deviationStatus = $_.DeviationStatus - licenseAvailable = $_.LicenseAvailable - currentValue = $_.CurrentValue - expectedValue = $_.ExpectedValue - latestDataCollection = $Row.LatestDataCollection + } catch { + Write-LogMessage -API $APIName -tenant $Row.TenantFilter -message "Failed to flatten alignment row for $($Row.TenantFilter)/$($Detail.StandardName): $($_.Exception.Message)" -sev Warning } } } @@ -106,7 +119,8 @@ function Invoke-ListTenantAlignment { Body = @($Results) }) } catch { - Write-LogMessage -API $APIName -message "Failed to get tenant alignment data: $($_.Exception.Message)" -sev Error + $ErrorDetail = "$($_.Exception.Message) at $($_.InvocationInfo.PositionMessage -replace '\r?\n', ' ')" + Write-LogMessage -API $APIName -message "Failed to get tenant alignment data: $ErrorDetail" -sev Error -LogData (Get-CippException -Exception $_) return ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::InternalServerError Body = @{ error = "Failed to get tenant alignment data: $($_.Exception.Message)" } From a0a79852f89fb82c0f5bab15822b46870198956e Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 10 Jun 2026 14:45:31 +0800 Subject: [PATCH 182/202] Update Invoke-ListTenantAlignment.ps1 --- .../Tenant/Standards/Invoke-ListTenantAlignment.ps1 | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListTenantAlignment.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListTenantAlignment.ps1 index 8a6b86259fcf..aae480c97d51 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListTenantAlignment.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListTenantAlignment.ps1 @@ -12,14 +12,9 @@ function Invoke-ListTenantAlignment { $APIName = $Request.Params.CIPPEndpoint $Granular = $Request.Query.granular -eq 'true' - $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter try { - $AlignmentParams = @{} - if ($TenantFilter -and $TenantFilter -ne 'AllTenants') { - $AlignmentParams.TenantFilter = $TenantFilter - } # Use the new Get-CIPPTenantAlignment function to get alignment data - $AlignmentData = Get-CIPPTenantAlignment @AlignmentParams + $AlignmentData = Get-CIPPTenantAlignment # Build a GUID -> displayName lookup from the templates table for all template types $TemplateLookup = @{} From e8d1342774427bee3572ebcb9f2aad8c0d30c800 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 10 Jun 2026 15:03:18 +0800 Subject: [PATCH 183/202] audit log detailed logging for debugging --- .../AuditLogs/New-CippAuditLogSearch.ps1 | 30 +++++++++++++++---- .../GraphHelper/New-GraphPOSTRequest.ps1 | 7 +++++ 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/Modules/CIPPCore/Public/AuditLogs/New-CippAuditLogSearch.ps1 b/Modules/CIPPCore/Public/AuditLogs/New-CippAuditLogSearch.ps1 index e135d29d6d74..fac6880e178b 100644 --- a/Modules/CIPPCore/Public/AuditLogs/New-CippAuditLogSearch.ps1 +++ b/Modules/CIPPCore/Public/AuditLogs/New-CippAuditLogSearch.ps1 @@ -153,9 +153,14 @@ function New-CippAuditLogSearch { } catch { $AuditLogError = $null $AuditLogErrorMessage = [string]$_.Exception.Message - $TrimmedAuditLogErrorMessage = $AuditLogErrorMessage.TrimStart() - if ($TrimmedAuditLogErrorMessage.StartsWith('{') -or $TrimmedAuditLogErrorMessage.StartsWith('[')) { - $AuditLogError = $AuditLogErrorMessage | ConvertFrom-Json -ErrorAction SilentlyContinue + $RawErrorBody = $_.Exception.Data['RawErrorBody'] + if ($RawErrorBody) { + $AuditLogError = [string]$RawErrorBody | ConvertFrom-Json -ErrorAction SilentlyContinue + } else { + $TrimmedAuditLogErrorMessage = $AuditLogErrorMessage.TrimStart() + if ($TrimmedAuditLogErrorMessage.StartsWith('{') -or $TrimmedAuditLogErrorMessage.StartsWith('[')) { + $AuditLogError = $AuditLogErrorMessage | ConvertFrom-Json -ErrorAction SilentlyContinue + } } if (($null -ne $AuditLogError) -and $AuditLogError.Status -eq 'AuditingDisabledTenant') { @@ -186,7 +191,12 @@ function New-CippAuditLogSearch { # Handle HTML error pages (e.g. Azure Front Door 502/504 gateway timeouts) if ($TrimmedAuditLogErrorMessage -match '([^<]+)') { $HtmlTitle = $Matches[1].Trim() - Write-LogMessage -API 'Audit Logs' -tenant $TenantFilter -message "Audit log search creation failed with gateway error for tenant $TenantFilter ($HtmlTitle)" -sev Warning + $GatewayLogData = [PSCustomObject]@{ + HtmlTitle = $HtmlTitle + NormalizedMessage = $AuditLogErrorMessage + RawResponseBody = if ($RawErrorBody) { [string]$RawErrorBody } else { $AuditLogErrorMessage } + } + Write-LogMessage -API 'Audit Logs' -tenant $TenantFilter -message "Audit log search creation failed with gateway error for tenant $TenantFilter ($HtmlTitle)" -sev Warning -LogData $GatewayLogData return [PSCustomObject]@{ id = $null displayName = [string]$DisplayName @@ -199,7 +209,17 @@ function New-CippAuditLogSearch { # Handle Microsoft-side timeouts / transient errors (e.g. UnknownError with empty message) $ErrorCode = $AuditLogError.error.code ?? $AuditLogError.code if ($ErrorCode -in @('UnknownError', 'ServiceUnavailable', 'RequestTimeout', 'GatewayTimeout', 'TooManyRequests')) { - Write-LogMessage -API 'Audit Logs' -tenant $TenantFilter -message "Audit log search creation failed with transient error for tenant $TenantFilter ($ErrorCode)" -sev Warning + $TransientLogData = [PSCustomObject]@{ + ErrorCode = $ErrorCode + ErrorMessage = $AuditLogError.error.message ?? $AuditLogError.message + InnerRequestId = $AuditLogError.error.innerError.'request-id' ?? $AuditLogError.error.innererror.'request-id' + InnerClientReqId = $AuditLogError.error.innerError.'client-request-id' ?? $AuditLogError.error.innererror.'client-request-id' + InnerErrorDate = $AuditLogError.error.innerError.date ?? $AuditLogError.error.innererror.date + NormalizedMessage = $AuditLogErrorMessage + RawResponseBody = if ($RawErrorBody) { [string]$RawErrorBody } else { $AuditLogErrorMessage } + ParsedError = $AuditLogError + } + Write-LogMessage -API 'Audit Logs' -tenant $TenantFilter -message "Audit log search creation failed for tenant $TenantFilter - Microsoft returned $ErrorCode" -sev Warning -LogData $TransientLogData return [PSCustomObject]@{ id = $null displayName = [string]$DisplayName diff --git a/Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1 index 5624e05108a3..4bdcf468ad9c 100644 --- a/Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1 @@ -45,6 +45,7 @@ function New-GraphPOSTRequest { $RetryCount = 0 $RequestSuccessful = $false + $RawErrorBody = $null do { try { Write-Information "$($type.ToUpper()) [ $uri ] | tenant: $tenantid | attempt: $($RetryCount + 1) of $maxRetries" @@ -53,6 +54,7 @@ function New-GraphPOSTRequest { } catch { $ShouldRetry = $false $WaitTime = 0 + $RawErrorBody = $_.ErrorDetails.Message $Message = if ($_.ErrorDetails.Message) { Get-NormalizedError -Message $_.ErrorDetails.Message } else { @@ -133,6 +135,11 @@ function New-GraphPOSTRequest { } if ($RequestSuccessful -eq $false) { + if ($RawErrorBody) { + $GraphException = [System.Exception]::new($Message) + $GraphException.Data['RawErrorBody'] = $RawErrorBody + throw $GraphException + } throw $Message } From b8fefcfdd085b5f25782f7f3da70324252f3c630 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 10 Jun 2026 15:20:53 +0800 Subject: [PATCH 184/202] Update Invoke-ExecMcp.ps1 --- .../Entrypoints/HTTP Functions/CIPP/MCP/Invoke-ExecMcp.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/MCP/Invoke-ExecMcp.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/MCP/Invoke-ExecMcp.ps1 index f2d9b427868a..f889b0a52090 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/MCP/Invoke-ExecMcp.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/MCP/Invoke-ExecMcp.ps1 @@ -11,7 +11,7 @@ function Invoke-ExecMcp { is enforced for each tool exactly as it would be for a normal API request. This endpoint's own role (CIPP.Core.Read) is only the floor required to use MCP at all. .FUNCTIONALITY - Entrypoint + Entrypoint,AnyTenant .ROLE CIPP.Core.Read #> From 83b130347aa81fb373cb77de32583dab9628e2e8 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 10 Jun 2026 22:33:53 +0800 Subject: [PATCH 185/202] bulk request next link following --- .../Public/GraphHelper/New-ExoBulkRequest.ps1 | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/Modules/CIPPCore/Public/GraphHelper/New-ExoBulkRequest.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-ExoBulkRequest.ps1 index 78a082a329c2..2fdf0196a560 100644 --- a/Modules/CIPPCore/Public/GraphHelper/New-ExoBulkRequest.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/New-ExoBulkRequest.ps1 @@ -44,6 +44,7 @@ function New-ExoBulkRequest { # Initialize the ID to Cmdlet Name mapping $IdToCmdletName = @{} $IdToOperationGuid = @{} # Track operation GUIDs when provided + $IdToBatchRequest = @{} # Original sub-requests, reused for nextLink continuations # Split the cmdletArray into batches of 10 $batches = [System.Collections.Generic.List[object]]::new() @@ -93,6 +94,7 @@ function New-ExoBulkRequest { # Map the Request ID to the Cmdlet Name and Operation GUID (if provided) $IdToCmdletName[$RequestId] = $cmd.CmdletInput.CmdletName + $IdToBatchRequest[$RequestId] = $BatchRequest if ($cmd.OperationGuid) { $IdToOperationGuid[$RequestId] = $cmd.OperationGuid } @@ -106,6 +108,49 @@ function New-ExoBulkRequest { Write-Host "Batch #$($batches.IndexOf($batch) + 1) of $($batches.Count) processed" } + + # Follow @odata.nextLink continuations so results are not capped at one page (mirrors New-GraphBulkRequest). + # The EXO admin API pages by re-POSTing the same CmdletInput body to the nextLink URL. + $IdToResponse = @{} + $NextLinkQueue = [System.Collections.Generic.Queue[object]]::new() + foreach ($Response in $ReturnedData) { + if ($Response.id -and -not $IdToResponse.ContainsKey($Response.id)) { + $IdToResponse[$Response.id] = $Response + } + if ($Response.body.'@odata.nextLink' -and $IdToBatchRequest.ContainsKey($Response.id)) { + $NextLinkQueue.Enqueue(@{ id = $Response.id; url = $Response.body.'@odata.nextLink' }) + } + } + + while ($NextLinkQueue.Count -gt 0) { + # Drain up to 10 nextLinks into a single $batch, same size as the main loop + $NextBatchRequests = [System.Collections.Generic.List[object]]::new() + while ($NextLinkQueue.Count -gt 0 -and $NextBatchRequests.Count -lt 10) { + $Item = $NextLinkQueue.Dequeue() + $ContinuationRequest = $IdToBatchRequest[$Item.id].Clone() + $ContinuationRequest['url'] = $Item.url + $NextBatchRequests.Add($ContinuationRequest) + } + + Write-Host "Fetching next page for $($NextBatchRequests.Count) request(s)" + $NextBatchBodyJson = ConvertTo-Json -InputObject @{ requests = @($NextBatchRequests) } -Depth 10 + $NextBatchBodyJson = Get-CIPPTextReplacement -TenantFilter $tenantid -Text $NextBatchBodyJson + $NextResults = Invoke-CIPPRestMethod $BatchURL -Method POST -Body $NextBatchBodyJson -Headers $Headers -ContentType 'application/json; charset=utf-8' + + foreach ($NextResponse in $NextResults.responses) { + $OriginalResponse = $IdToResponse[$NextResponse.id] + if (-not $OriginalResponse) { continue } + if ($NextResponse.body.value) { + $MergedValues = [System.Collections.Generic.List[object]]::new() + foreach ($val in @($OriginalResponse.body.value)) { $MergedValues.Add($val) } + foreach ($val in @($NextResponse.body.value)) { $MergedValues.Add($val) } + $OriginalResponse.body.value = $MergedValues + } + if ($NextResponse.body.'@odata.nextLink') { + $NextLinkQueue.Enqueue(@{ id = $NextResponse.id; url = $NextResponse.body.'@odata.nextLink' }) + } + } + } } catch { # Error handling (omitted for brevity) } From 9f0bacd62c74d6e65ee0f51623884569de2eb8c1 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 10 Jun 2026 22:49:50 +0800 Subject: [PATCH 186/202] manual pagination support for Invoke-ListMailQuarantine --- .../Spamfilter/Invoke-ListMailQuarantine.ps1 | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListMailQuarantine.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListMailQuarantine.ps1 index bf523175d64b..6e608766541d 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListMailQuarantine.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListMailQuarantine.ps1 @@ -14,15 +14,27 @@ function Invoke-ListMailQuarantine { try { $GraphRequest = if ($TenantFilter -ne 'AllTenants') { - $Page = 1 $PageSize = 1000 - $AllMessages = [System.Collections.Generic.List[object]]::new() - do { + if ($Request.Query.manualPagination -and [System.Convert]::ToBoolean($Request.Query.manualPagination)) { + # Manual pagination: return one page per request. The frontend chains requests via + # Metadata.nextLink, which for this endpoint is the next Get-QuarantineMessage page number. + $Page = if ($Request.Query.nextLink -match '^\d+$') { [int]$Request.Query.nextLink } else { 1 } $Results = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-QuarantineMessage' -cmdParams @{ PageSize = $PageSize; Page = $Page } | Select-Object -ExcludeProperty *data.type* - if ($Results) { $AllMessages.AddRange(@($Results)) } - $Page++ - } while (@($Results).Count -eq $PageSize) - $AllMessages + # Get-QuarantineMessage supports a maximum Page of 1000 + if (@($Results).Count -eq $PageSize -and $Page -lt 1000) { + $Metadata = [PSCustomObject]@{ nextLink = [string]($Page + 1) } + } + $Results + } else { + $Page = 1 + $AllMessages = [System.Collections.Generic.List[object]]::new() + do { + $Results = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-QuarantineMessage' -cmdParams @{ PageSize = $PageSize; Page = $Page } | Select-Object -ExcludeProperty *data.type* + if ($Results) { $AllMessages.AddRange(@($Results)) } + $Page++ + } while (@($Results).Count -eq $PageSize) + $AllMessages + } } else { $Table = Get-CIPPTable -TableName cacheQuarantineMessages $PartitionKey = 'QuarantineMessage' From cc84a49df1633a1b5de2dfdeda991f9d8b7b660c Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:22:58 +0800 Subject: [PATCH 187/202] Fix ORCA104 --- .../ORCA/Identity/Invoke-CippTestORCA104.ps1 | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA104.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA104.ps1 index 2382b39b8301..0d320414f579 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA104.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA104.ps1 @@ -6,27 +6,27 @@ function Invoke-CippTestORCA104 { param($Tenant) try { - $AntiPhishPolicies = Get-CIPPTestData -TenantFilter $Tenant -Type 'ExoAntiPhishPolicies' + $Policies = Get-CIPPTestData -TenantFilter $Tenant -Type 'ExoHostedContentFilterPolicy' - if (-not $AntiPhishPolicies) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA104' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'High Confidence Phish action set to Quarantine message' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Anti-Phish' + if (-not $Policies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA104' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'High Confidence Phish action set to Quarantine message' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Anti-Spam' return } $FailedPolicies = [System.Collections.Generic.List[object]]::new() $PassedPolicies = [System.Collections.Generic.List[object]]::new() - foreach ($Policy in $AntiPhishPolicies) { + foreach ($Policy in $Policies) { if ($Policy.HighConfidencePhishAction -eq 'Quarantine') { - $PassedPolicies.Add($Policy) + $PassedPolicies.Add($Policy) | Out-Null } else { - $FailedPolicies.Add($Policy) + $FailedPolicies.Add($Policy) | Out-Null } } if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = [System.Text.StringBuilder]::new("All anti-phishing policies have High Confidence Phish action set to Quarantine.`n`n") + $Result = [System.Text.StringBuilder]::new("All anti-spam policies have High Confidence Phish action set to Quarantine.`n`n") $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)`n`n") if ($PassedPolicies.Count -gt 0) { $null = $Result.Append("| Policy Name | Action |`n") @@ -37,7 +37,7 @@ function Invoke-CippTestORCA104 { } } else { $Status = 'Failed' - $Result = [System.Text.StringBuilder]::new("Some anti-phishing policies do not have High Confidence Phish action set to Quarantine.`n`n") + $Result = [System.Text.StringBuilder]::new("Some anti-spam policies do not have High Confidence Phish action set to Quarantine.`n`n") $null = $Result.Append("**Failed Policies:** $($FailedPolicies.Count) | **Passed Policies:** $($PassedPolicies.Count)`n`n") $null = $Result.Append("### Non-Compliant Policies`n`n") $null = $Result.Append("| Policy Name | Current Action | Recommended Action |`n") @@ -48,10 +48,10 @@ function Invoke-CippTestORCA104 { $null = $Result.Append("`n**Remediation:** Update the HighConfidencePhishAction to 'Quarantine' for enhanced security.") } - Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA104' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'High Confidence Phish action set to Quarantine message' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Anti-Phish' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA104' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'High Confidence Phish action set to Quarantine message' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Anti-Spam' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA104' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'High Confidence Phish action set to Quarantine message' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Anti-Phish' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA104' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'High Confidence Phish action set to Quarantine message' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Anti-Spam' } } From 8cd8d1d94e51ce793d79567822483073715392ad Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:51:56 +0800 Subject: [PATCH 188/202] Fix for ORCA107 and add Exchange Global Quarantine policy to cache --- Config/CIPPDBCacheTypes.json | 5 ++ .../Set-CIPPDBCacheExoQuarantinePolicy.ps1 | 14 ++++++ .../ORCA/Identity/Invoke-CippTestORCA107.ps1 | 46 ++++++++++++------- 3 files changed, 49 insertions(+), 16 deletions(-) diff --git a/Config/CIPPDBCacheTypes.json b/Config/CIPPDBCacheTypes.json index eb833092befd..639f3ffb90d1 100644 --- a/Config/CIPPDBCacheTypes.json +++ b/Config/CIPPDBCacheTypes.json @@ -214,6 +214,11 @@ "friendlyName": "Exchange Quarantine Policy", "description": "Exchange Online quarantine policy" }, + { + "type": "ExoGlobalQuarantinePolicy", + "friendlyName": "Exchange Global Quarantine Policy", + "description": "Exchange Online tenant-wide Global Quarantine policy (end-user notification settings)" + }, { "type": "ExoRemoteDomain", "friendlyName": "Exchange Remote Domain", diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoQuarantinePolicy.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoQuarantinePolicy.ps1 index cd7fb1afbbd1..63cb47149ae2 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoQuarantinePolicy.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoQuarantinePolicy.ps1 @@ -29,4 +29,18 @@ function Set-CIPPDBCacheExoQuarantinePolicy { } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache Quarantine policy data: $($_.Exception.Message)" -sev Error } + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Global Quarantine policy' -sev Debug + + $GlobalQuarantinePolicy = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-QuarantinePolicy' -cmdParams @{ QuarantinePolicyType = 'GlobalQuarantinePolicy' } + if ($GlobalQuarantinePolicy) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoGlobalQuarantinePolicy' -Data $GlobalQuarantinePolicy -AddCount + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached Global Quarantine policy' -sev Debug + } + $GlobalQuarantinePolicy = $null + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache Global Quarantine policy data: $($_.Exception.Message)" -sev Error + } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA107.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA107.ps1 index 90655ffcf9c1..d962fa7b91b0 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA107.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA107.ps1 @@ -6,45 +6,59 @@ function Invoke-CippTestORCA107 { param($Tenant) try { - $Policies = Get-CIPPTestData -TenantFilter $Tenant -Type 'ExoQuarantinePolicy' + $Policies = Get-CIPPTestData -TenantFilter $Tenant -Type 'ExoGlobalQuarantinePolicy' if (-not $Policies) { Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA107' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Low' -Name 'End-user spam notification is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Quarantine' return } + # Exo returns EndUserSpamNotificationFrequency as an ISO 8601 duration string ('PT4H', 'P1D', 'P7D'). + # 'PT0S' or null means notifications are disabled. The placeholder policy name 'DefaultGlobalPolicy' + # indicates the global policy has never been configured. $FailedPolicies = [System.Collections.Generic.List[object]]::new() $PassedPolicies = [System.Collections.Generic.List[object]]::new() foreach ($Policy in $Policies) { - if ($Policy.EndUserSpamNotificationFrequency -gt 0) { - $PassedPolicies.Add($Policy) | Out-Null + $Frequency = $Policy.EndUserSpamNotificationFrequency + $IsConfigured = $Policy.Name -ne 'DefaultGlobalPolicy' + $IsEnabled = $false + if ($IsConfigured -and $Frequency) { + try { + $TimeSpan = [System.Xml.XmlConvert]::ToTimeSpan([string]$Frequency) + $IsEnabled = $TimeSpan.TotalSeconds -gt 0 + } catch { + $IsEnabled = $false + } + } + + $DisplayFrequency = if ($Frequency) { [string]$Frequency } else { 'Not set' } + $Annotated = $Policy | Select-Object *, @{ Name = 'DisplayFrequency'; Expression = { $DisplayFrequency } } + + if ($IsEnabled) { + $PassedPolicies.Add($Annotated) | Out-Null } else { - $FailedPolicies.Add($Policy) | Out-Null + $FailedPolicies.Add($Annotated) | Out-Null } } if ($FailedPolicies.Count -eq 0 -and $PassedPolicies.Count -gt 0) { $Status = 'Passed' - $Result = [System.Text.StringBuilder]::new("All quarantine policies have end-user spam notifications enabled.`n`n") - $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)`n`n") - $null = $Result.Append("| Policy Name | Notification Frequency (days) |`n") - $null = $Result.Append("|------------|-------------------------------|`n") + $Result = [System.Text.StringBuilder]::new("The Global Quarantine policy has end-user spam notifications enabled.`n`n") + $null = $Result.Append("| Policy Name | Notification Frequency |`n") + $null = $Result.Append("|------------|------------------------|`n") foreach ($Policy in $PassedPolicies) { - $null = $Result.Append("| $($Policy.Identity) | $($Policy.EndUserSpamNotificationFrequency) |`n") + $null = $Result.Append("| $($Policy.Identity) | $($Policy.DisplayFrequency) |`n") } - } elseif ($PassedPolicies.Count -eq 0) { - $Status = 'Failed' - $Result = [System.Text.StringBuilder]::new("No quarantine policies have end-user spam notifications enabled.`n`n") } else { $Status = 'Failed' - $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) quarantine policies do not have end-user spam notifications enabled.`n`n") - $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") + $Result = [System.Text.StringBuilder]::new("The Global Quarantine policy does not have end-user spam notifications enabled.`n`n") $null = $Result.Append("| Policy Name | Notification Frequency |`n") - $null = $Result.Append("|------------|----------------------|`n") + $null = $Result.Append("|------------|------------------------|`n") foreach ($Policy in $FailedPolicies) { - $null = $Result.Append("| $($Policy.Identity) | Disabled |`n") + $null = $Result.Append("| $($Policy.Identity) | $($Policy.DisplayFrequency) |`n") } + $null = $Result.Append("`n**Remediation:** Configure the Global Quarantine policy with a notification frequency (e.g. PT4H, P1D, or P7D) via `Set-QuarantinePolicy -EndUserSpamNotificationFrequency`.") } Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA107' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Low' -Name 'End-user spam notification is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Quarantine' From 2ab0e0e27c77d146301b427365e0d305e813137d Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 10 Jun 2026 12:06:17 -0400 Subject: [PATCH 189/202] fix: rerun issue --- Modules/CIPPCore/Public/Test-CIPPRerun.ps1 | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Modules/CIPPCore/Public/Test-CIPPRerun.ps1 b/Modules/CIPPCore/Public/Test-CIPPRerun.ps1 index f59727017896..6a259ea06a9d 100644 --- a/Modules/CIPPCore/Public/Test-CIPPRerun.ps1 +++ b/Modules/CIPPCore/Public/Test-CIPPRerun.ps1 @@ -62,9 +62,9 @@ function Test-CIPPRerun { $NewSettings = $($Settings | ConvertTo-Json -Depth 10 -Compress) if ($NewSettings.Length -ne $PreviousSettings.Length) { Write-Host "$($NewSettings.Length) vs $($PreviousSettings.Length) - settings have changed." - $RerunData.EstimatedNextRun = $EstimatedNextRun - $RerunData.LastScheduledTime = "$CurrentUnixTime" - $RerunData.Settings = "$($Settings | ConvertTo-Json -Depth 10 -Compress)" + $RerunData | Add-Member -MemberType NoteProperty -Name 'EstimatedNextRun' -Value $EstimatedNextRun -Force + $RerunData | Add-Member -MemberType NoteProperty -Name 'LastScheduledTime' -Value "$CurrentUnixTime" -Force + $RerunData | Add-Member -MemberType NoteProperty -Name 'Settings' -Value "$($Settings | ConvertTo-Json -Depth 10 -Compress)" -Force Add-CIPPAzDataTableEntity @RerunTable -Entity $RerunData -Force return $false # Not a rerun because settings have changed. } @@ -73,9 +73,9 @@ function Test-CIPPRerun { # treat it as a new execution rather than a duplicate. if ($BaseTime -gt 0 -and $RerunData.LastScheduledTime -and [int64]$RerunData.LastScheduledTime -ne $BaseTime) { Write-Information "Task $API has a new ScheduledTime ($BaseTime vs cached $($RerunData.LastScheduledTime)). Treating as new execution." - $RerunData.EstimatedNextRun = $EstimatedNextRun - $RerunData.LastScheduledTime = "$BaseTime" - $RerunData.Settings = "$($Settings | ConvertTo-Json -Depth 10 -Compress)" + $RerunData | Add-Member -MemberType NoteProperty -Name 'EstimatedNextRun' -Value $EstimatedNextRun -Force + $RerunData | Add-Member -MemberType NoteProperty -Name 'LastScheduledTime' -Value "$BaseTime" -Force + $RerunData | Add-Member -MemberType NoteProperty -Name 'Settings' -Value "$($Settings | ConvertTo-Json -Depth 10 -Compress)" -Force Add-CIPPAzDataTableEntity @RerunTable -Entity $RerunData -Force return $false } @@ -83,9 +83,9 @@ function Test-CIPPRerun { Write-LogMessage -API $API -message "$Type rerun detected for $($API). Prevented from running again." -tenant $TenantFilter -headers $Headers -Sev 'Info' return $true } else { - $RerunData.EstimatedNextRun = $EstimatedNextRun - $RerunData.LastScheduledTime = "$BaseTime" - $RerunData.Settings = "$($Settings | ConvertTo-Json -Depth 10 -Compress)" + $RerunData | Add-Member -MemberType NoteProperty -Name 'EstimatedNextRun' -Value $EstimatedNextRun -Force + $RerunData | Add-Member -MemberType NoteProperty -Name 'LastScheduledTime' -Value "$BaseTime" -Force + $RerunData | Add-Member -MemberType NoteProperty -Name 'Settings' -Value "$($Settings | ConvertTo-Json -Depth 10 -Compress)" -Force Add-CIPPAzDataTableEntity @RerunTable -Entity $RerunData -Force return $false } From 238001c13e0d62a7f13b00d46c99c7be82c11cad Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 11 Jun 2026 00:13:53 +0800 Subject: [PATCH 190/202] Update policy based on MS and Orca guidance --- .../Invoke-CIPPStandardSpamFilterPolicy.ps1 | 32 +++++++++---------- .../ORCA/Identity/Invoke-CippTestORCA102.ps1 | 24 +++++++++++--- 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 index f01a369d3406..a305b1510973 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 @@ -7,8 +7,8 @@ function Invoke-CIPPStandardSpamFilterPolicy { .SYNOPSIS (Label) Default Spam Filter Policy .DESCRIPTION - (Helptext) This standard creates a Spam filter policy similar to the default strict policy. - (DocsDescription) This standard creates a Spam filter policy similar to the default strict policy, the following settings are configured to on by default: IncreaseScoreWithNumericIps, IncreaseScoreWithRedirectToOtherPort, MarkAsSpamEmptyMessages, MarkAsSpamJavaScriptInHtml, MarkAsSpamSpfRecordHardFail, MarkAsSpamFromAddressAuthFail, MarkAsSpamNdrBackscatter, MarkAsSpamBulkMail, InlineSafetyTipsEnabled, PhishZapEnabled, SpamZapEnabled + (Helptext) This standard creates a Spam filter policy aligned with the Microsoft Strict preset. + (DocsDescription) This standard creates a Spam filter policy aligned with the Microsoft Strict preset. All Advanced Spam Filter (ASF) settings are left Off per Microsoft guidance (ASF is deprecated and prevents false-positive reporting). The following settings are configured On by default: MarkAsSpamBulkMail, InlineSafetyTipsEnabled, PhishZapEnabled, SpamZapEnabled. .NOTES CAT Defender Standards @@ -127,20 +127,20 @@ function Invoke-CIPPStandardSpamFilterPolicy { ($CurrentState.BulkThreshold -eq [int]$Settings.BulkThreshold) -and ($CurrentState.QuarantineRetentionPeriod -eq 30) -and ($CurrentState.IncreaseScoreWithImageLinks -eq $IncreaseScoreWithImageLinks) -and - ($CurrentState.IncreaseScoreWithNumericIps -eq 'On') -and - ($CurrentState.IncreaseScoreWithRedirectToOtherPort -eq 'On') -and + ($CurrentState.IncreaseScoreWithNumericIps -eq 'Off') -and + ($CurrentState.IncreaseScoreWithRedirectToOtherPort -eq 'Off') -and ($CurrentState.IncreaseScoreWithBizOrInfoUrls -eq $IncreaseScoreWithBizOrInfoUrls) -and - ($CurrentState.MarkAsSpamEmptyMessages -eq 'On') -and - ($CurrentState.MarkAsSpamJavaScriptInHtml -eq 'On') -and + ($CurrentState.MarkAsSpamEmptyMessages -eq 'Off') -and + ($CurrentState.MarkAsSpamJavaScriptInHtml -eq 'Off') -and ($CurrentState.MarkAsSpamFramesInHtml -eq $MarkAsSpamFramesInHtml) -and ($CurrentState.MarkAsSpamObjectTagsInHtml -eq $MarkAsSpamObjectTagsInHtml) -and ($CurrentState.MarkAsSpamEmbedTagsInHtml -eq $MarkAsSpamEmbedTagsInHtml) -and ($CurrentState.MarkAsSpamFormTagsInHtml -eq $MarkAsSpamFormTagsInHtml) -and ($CurrentState.MarkAsSpamWebBugsInHtml -eq $MarkAsSpamWebBugsInHtml) -and ($CurrentState.MarkAsSpamSensitiveWordList -eq $MarkAsSpamSensitiveWordList) -and - ($CurrentState.MarkAsSpamSpfRecordHardFail -eq 'On') -and - ($CurrentState.MarkAsSpamFromAddressAuthFail -eq 'On') -and - ($CurrentState.MarkAsSpamNdrBackscatter -eq 'On') -and + ($CurrentState.MarkAsSpamSpfRecordHardFail -eq 'Off') -and + ($CurrentState.MarkAsSpamFromAddressAuthFail -eq 'Off') -and + ($CurrentState.MarkAsSpamNdrBackscatter -eq 'Off') -and ($CurrentState.MarkAsSpamBulkMail -eq 'On') -and ($CurrentState.InlineSafetyTipsEnabled -eq $true) -and ($CurrentState.PhishZapEnabled -eq $true) -and @@ -183,20 +183,20 @@ function Invoke-CIPPStandardSpamFilterPolicy { BulkThreshold = [int]$Settings.BulkThreshold QuarantineRetentionPeriod = 30 IncreaseScoreWithImageLinks = $IncreaseScoreWithImageLinks - IncreaseScoreWithNumericIps = 'On' - IncreaseScoreWithRedirectToOtherPort = 'On' + IncreaseScoreWithNumericIps = 'Off' + IncreaseScoreWithRedirectToOtherPort = 'Off' IncreaseScoreWithBizOrInfoUrls = $IncreaseScoreWithBizOrInfoUrls - MarkAsSpamEmptyMessages = 'On' - MarkAsSpamJavaScriptInHtml = 'On' + MarkAsSpamEmptyMessages = 'Off' + MarkAsSpamJavaScriptInHtml = 'Off' MarkAsSpamFramesInHtml = $MarkAsSpamFramesInHtml MarkAsSpamObjectTagsInHtml = $MarkAsSpamObjectTagsInHtml MarkAsSpamEmbedTagsInHtml = $MarkAsSpamEmbedTagsInHtml MarkAsSpamFormTagsInHtml = $MarkAsSpamFormTagsInHtml MarkAsSpamWebBugsInHtml = $MarkAsSpamWebBugsInHtml MarkAsSpamSensitiveWordList = $MarkAsSpamSensitiveWordList - MarkAsSpamSpfRecordHardFail = 'On' - MarkAsSpamFromAddressAuthFail = 'On' - MarkAsSpamNdrBackscatter = 'On' + MarkAsSpamSpfRecordHardFail = 'Off' + MarkAsSpamFromAddressAuthFail = 'Off' + MarkAsSpamNdrBackscatter = 'Off' MarkAsSpamBulkMail = 'On' InlineSafetyTipsEnabled = $true PhishZapEnabled = $true diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA102.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA102.ps1 index 5d4459f46995..747442bd974c 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA102.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA102.ps1 @@ -54,12 +54,28 @@ function Invoke-CippTestORCA102 { $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") $null = $Result.Append("| Policy Name | Enabled ASF Options |`n") $null = $Result.Append("|------------|---------------------|`n") + $ASFSettingMap = [ordered]@{ + IncreaseScoreWithImageLinks = 'ImageLinks' + IncreaseScoreWithNumericIps = 'NumericIPs' + IncreaseScoreWithRedirectToOtherPort = 'RedirectToOtherPort' + IncreaseScoreWithBizOrInfoUrls = 'BizOrInfoUrls' + MarkAsSpamEmptyMessages = 'EmptyMessages' + MarkAsSpamJavaScriptInHtml = 'JavaScript' + MarkAsSpamFramesInHtml = 'Frames' + MarkAsSpamObjectTagsInHtml = 'ObjectTags' + MarkAsSpamEmbedTagsInHtml = 'EmbedTags' + MarkAsSpamFormTagsInHtml = 'FormTags' + MarkAsSpamWebBugsInHtml = 'WebBugs' + MarkAsSpamSensitiveWordList = 'SensitiveWordList' + MarkAsSpamSpfRecordHardFail = 'SpfRecordHardFail' + MarkAsSpamFromAddressAuthFail = 'FromAddressAuthFail' + MarkAsSpamNdrBackscatter = 'NdrBackscatter' + } foreach ($Policy in $FailedPolicies) { $EnabledOptions = [System.Collections.Generic.List[string]]::new() - if ($Policy.IncreaseScoreWithImageLinks -eq 'On') { $EnabledOptions.Add('ImageLinks') | Out-Null } - if ($Policy.IncreaseScoreWithNumericIps -eq 'On') { $EnabledOptions.Add('NumericIPs') | Out-Null } - if ($Policy.MarkAsSpamEmptyMessages -eq 'On') { $EnabledOptions.Add('EmptyMessages') | Out-Null } - if ($Policy.MarkAsSpamJavaScriptInHtml -eq 'On') { $EnabledOptions.Add('JavaScript') | Out-Null } + foreach ($Property in $ASFSettingMap.Keys) { + if ($Policy.$Property -eq 'On') { $EnabledOptions.Add($ASFSettingMap[$Property]) | Out-Null } + } $null = $Result.Append("| $($Policy.Identity) | $($EnabledOptions -join ', ') |`n") } } From 0640f07ccdf22a8294a91502def41b987be84ab5 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 11 Jun 2026 00:25:11 +0800 Subject: [PATCH 191/202] Fixes ORCA179 --- .../Tests/ORCA/Identity/Invoke-CippTestORCA179.ps1 | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA179.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA179.ps1 index c2ea2cf19ed6..9013546daa21 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA179.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA179.ps1 @@ -13,10 +13,15 @@ function Invoke-CippTestORCA179 { return } + # Exclude the Built-In Protection preset — Microsoft scopes it to external senders by design. + $CustomPolicies = $Policies | Where-Object { + $_.IsBuiltInProtection -ne $true + } + $FailedPolicies = [System.Collections.Generic.List[object]]::new() $PassedPolicies = [System.Collections.Generic.List[object]]::new() - foreach ($Policy in $Policies) { + foreach ($Policy in $CustomPolicies) { if ($Policy.EnableForInternalSenders -eq $true) { $PassedPolicies.Add($Policy) | Out-Null } else { @@ -24,10 +29,13 @@ function Invoke-CippTestORCA179 { } } - if ($FailedPolicies.Count -eq 0) { + if ($PassedPolicies.Count -gt 0 -and $FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = [System.Text.StringBuilder]::new("All Safe Links policies are enabled for internal senders.`n`n") + $Result = [System.Text.StringBuilder]::new("All custom Safe Links policies are enabled for internal senders.`n`n") $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)") + } elseif ($PassedPolicies.Count -eq 0 -and $FailedPolicies.Count -eq 0) { + $Status = 'Failed' + $Result = [System.Text.StringBuilder]::new("No custom Safe Links policies are configured. The Built-In Protection policy does not cover internal senders.`n`n**Remediation:** Create a custom Safe Links policy with `EnableForInternalSenders = `$true`.") } else { $Status = 'Failed' $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) Safe Links policies are not enabled for internal senders.`n`n") From db3de75a6c6fc7cbf6e1c775ffb485e9ca2a215d Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 11 Jun 2026 00:26:36 +0800 Subject: [PATCH 192/202] Fixes ORCA244 --- .../ORCA/Identity/Invoke-CippTestORCA244.ps1 | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA244.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA244.ps1 index 7b413894a8a4..f3146399098c 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA244.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA244.ps1 @@ -6,10 +6,10 @@ function Invoke-CippTestORCA244 { param($Tenant) try { - $Policies = Get-CIPPTestData -TenantFilter $Tenant -Type 'ExoHostedContentFilterPolicy' + $Policies = Get-CIPPTestData -TenantFilter $Tenant -Type 'ExoAntiPhishPolicies' if (-not $Policies) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA244' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Policies honor sending domain DMARC' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Spam' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA244' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Policies honor sending domain DMARC' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Phish' return } @@ -17,7 +17,7 @@ function Invoke-CippTestORCA244 { $PassedPolicies = [System.Collections.Generic.List[object]]::new() foreach ($Policy in $Policies) { - if ($Policy.HonorDMARCPolicy -eq $true) { + if ($Policy.HonorDmarcPolicy -eq $true) { $PassedPolicies.Add($Policy) | Out-Null } else { $FailedPolicies.Add($Policy) | Out-Null @@ -26,24 +26,24 @@ function Invoke-CippTestORCA244 { if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = [System.Text.StringBuilder]::new("All anti-spam policies honor sending domain DMARC.`n`n") + $Result = [System.Text.StringBuilder]::new("All anti-phishing policies honor sending domain DMARC.`n`n") $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)") } else { $Status = 'Failed' - $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) anti-spam policies do not honor sending domain DMARC.`n`n") + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) anti-phishing policies do not honor sending domain DMARC.`n`n") $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") $null = $Result.Append("| Policy Name | Honor DMARC Policy |`n") $null = $Result.Append("|------------|--------------------|`n") foreach ($Policy in $FailedPolicies) { - $null = $Result.Append("| $($Policy.Identity) | $($Policy.HonorDMARCPolicy) |`n") + $null = $Result.Append("| $($Policy.Identity) | $($Policy.HonorDmarcPolicy) |`n") } } - Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA244' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Policies honor sending domain DMARC' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Spam' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA244' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Policies honor sending domain DMARC' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Phish' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA244' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Policies honor sending domain DMARC' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Spam' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA244' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Policies honor sending domain DMARC' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Phish' } } From a556b98bdbd08fb9d97df79ae8c79f4d47011e39 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 11 Jun 2026 00:34:47 +0800 Subject: [PATCH 193/202] Fixes ORCA113 --- .../ORCA/Identity/Invoke-CippTestORCA113.ps1 | 40 +++++++++++-------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA113.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA113.ps1 index af98497bee2a..9a455e4b5661 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA113.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA113.ps1 @@ -13,39 +13,45 @@ function Invoke-CippTestORCA113 { return } + # Exclude the Built-In Protection preset — Microsoft owns it and intentionally allows click-through on the baseline. + $CustomPolicies = $SafeLinksPolicies | Where-Object { + $_.IsBuiltInProtection -ne $true + } + $FailedPolicies = [System.Collections.Generic.List[object]]::new() $PassedPolicies = [System.Collections.Generic.List[object]]::new() - foreach ($Policy in $SafeLinksPolicies) { - if ($Policy.DoNotAllowClickThrough -eq $true) { - $PassedPolicies.Add($Policy) + foreach ($Policy in $CustomPolicies) { + if ($Policy.AllowClickThrough -eq $false) { + $PassedPolicies.Add($Policy) | Out-Null } else { - $FailedPolicies.Add($Policy) + $FailedPolicies.Add($Policy) | Out-Null } } - if ($FailedPolicies.Count -eq 0) { + if ($PassedPolicies.Count -gt 0 -and $FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = [System.Text.StringBuilder]::new("All Safe Links policies have click-through disabled (DoNotAllowClickThrough = true).`n`n") + $Result = [System.Text.StringBuilder]::new("All custom Safe Links policies have click-through disabled (AllowClickThrough = false).`n`n") $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)`n`n") - if ($PassedPolicies.Count -gt 0) { - $null = $Result.Append("| Policy Name | DoNotAllowClickThrough |`n") - $null = $Result.Append("|------------|----------------------|`n") - foreach ($Policy in $PassedPolicies) { - $null = $Result.Append("| $($Policy.Identity) | $($Policy.DoNotAllowClickThrough) |`n") - } + $null = $Result.Append("| Policy Name | AllowClickThrough |`n") + $null = $Result.Append("|------------|-------------------|`n") + foreach ($Policy in $PassedPolicies) { + $null = $Result.Append("| $($Policy.Identity) | $($Policy.AllowClickThrough) |`n") } + } elseif ($PassedPolicies.Count -eq 0 -and $FailedPolicies.Count -eq 0) { + $Status = 'Failed' + $Result = [System.Text.StringBuilder]::new("No custom Safe Links policies are configured. The Built-In Protection policy allows click-through by design.`n`n**Remediation:** Create a custom Safe Links policy with `AllowClickThrough = `$false`.") } else { $Status = 'Failed' - $Result = [System.Text.StringBuilder]::new("Some Safe Links policies allow click-through, which reduces protection.`n`n") + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) custom Safe Links policies allow click-through, which reduces protection.`n`n") $null = $Result.Append("**Failed Policies:** $($FailedPolicies.Count) | **Passed Policies:** $($PassedPolicies.Count)`n`n") $null = $Result.Append("### Non-Compliant Policies`n`n") - $null = $Result.Append("| Policy Name | DoNotAllowClickThrough | Recommended |`n") - $null = $Result.Append("|------------|----------------------|-------------|`n") + $null = $Result.Append("| Policy Name | AllowClickThrough | Recommended |`n") + $null = $Result.Append("|------------|-------------------|-------------|`n") foreach ($Policy in $FailedPolicies) { - $null = $Result.Append("| $($Policy.Identity) | $($Policy.DoNotAllowClickThrough) | true |`n") + $null = $Result.Append("| $($Policy.Identity) | $($Policy.AllowClickThrough) | false |`n") } - $null = $Result.Append("`n**Remediation:** Disable click-through (set DoNotAllowClickThrough to true) to prevent users from bypassing Safe Links protection.") + $null = $Result.Append("`n**Remediation:** Set AllowClickThrough to false to prevent users from bypassing Safe Links protection.") } Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA113' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'AllowClickThrough is disabled in Safe Links policies' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Safe Links' From cbcc61b5afd8c7ea9bb9bef1da7878465afc5610 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 11 Jun 2026 00:48:34 +0800 Subject: [PATCH 194/202] Fixes ORCA103 --- .../Tests/ORCA/Identity/Invoke-CippTestORCA103.ps1 | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA103.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA103.ps1 index 8ee002e8f83e..ff2fa2fe11d0 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA103.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA103.ps1 @@ -20,17 +20,17 @@ function Invoke-CippTestORCA103 { $IsCompliant = $true $Issues = [System.Collections.Generic.List[string]]::new() - if ($Policy.RecipientLimitExternalPerHour -ne 500) { + if ($Policy.RecipientLimitExternalPerHour -le 0 -or $Policy.RecipientLimitExternalPerHour -gt 500) { $IsCompliant = $false - $Issues.Add("RecipientLimitExternalPerHour: $($Policy.RecipientLimitExternalPerHour) (should be 500)") | Out-Null + $Issues.Add("RecipientLimitExternalPerHour: $($Policy.RecipientLimitExternalPerHour) (should be between 1 and 500)") | Out-Null } - if ($Policy.RecipientLimitInternalPerHour -ne 1000) { + if ($Policy.RecipientLimitInternalPerHour -le 0 -or $Policy.RecipientLimitInternalPerHour -gt 1000) { $IsCompliant = $false - $Issues.Add("RecipientLimitInternalPerHour: $($Policy.RecipientLimitInternalPerHour) (should be 1000)") | Out-Null + $Issues.Add("RecipientLimitInternalPerHour: $($Policy.RecipientLimitInternalPerHour) (should be between 1 and 1000)") | Out-Null } - if ($Policy.ActionWhenThresholdReached -ne 'BlockUserForToday') { + if ($Policy.ActionWhenThresholdReached -ne 'BlockUser') { $IsCompliant = $false - $Issues.Add("ActionWhenThresholdReached: $($Policy.ActionWhenThresholdReached) (should be BlockUserForToday)") | Out-Null + $Issues.Add("ActionWhenThresholdReached: $($Policy.ActionWhenThresholdReached) (should be BlockUser)") | Out-Null } if ($IsCompliant) { From 2851db0f52d88c4a7b5ee0d1a739a7d9d95ea27b Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 11 Jun 2026 01:17:20 +0800 Subject: [PATCH 195/202] Fixes ORCA233_1 --- Config/CIPPDBCacheTypes.json | 5 ++ .../Public/Invoke-CIPPDBCacheCollection.ps1 | 1 + .../Set-CIPPDBCacheExoInboundConnector.ps1 | 32 +++++++ .../Identity/Invoke-CippTestORCA233_1.ps1 | 84 +++++++++++++++---- 4 files changed, 104 insertions(+), 18 deletions(-) create mode 100644 Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoInboundConnector.ps1 diff --git a/Config/CIPPDBCacheTypes.json b/Config/CIPPDBCacheTypes.json index 639f3ffb90d1..7ee7d80240ed 100644 --- a/Config/CIPPDBCacheTypes.json +++ b/Config/CIPPDBCacheTypes.json @@ -244,6 +244,11 @@ "friendlyName": "Exchange Tenant Allow/Block List", "description": "Exchange Online tenant allow/block list" }, + { + "type": "ExoInboundConnector", + "friendlyName": "Exchange Inbound Connectors", + "description": "Exchange Online inbound connectors (includes enhanced filtering settings)" + }, { "type": "Mailboxes", "friendlyName": "Mailboxes", diff --git a/Modules/CIPPCore/Public/Invoke-CIPPDBCacheCollection.ps1 b/Modules/CIPPCore/Public/Invoke-CIPPDBCacheCollection.ps1 index 2538fc8493fb..13539fbfbe1b 100644 --- a/Modules/CIPPCore/Public/Invoke-CIPPDBCacheCollection.ps1 +++ b/Modules/CIPPCore/Public/Invoke-CIPPDBCacheCollection.ps1 @@ -87,6 +87,7 @@ function Invoke-CIPPDBCacheCollection { 'ExoAdminAuditLogConfig' 'ExoPresetSecurityPolicy' 'ExoTenantAllowBlockList' + 'ExoInboundConnector' 'OwaMailboxPolicy' 'ReportSubmissionPolicy' 'ExoTransportConfig' diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoInboundConnector.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoInboundConnector.ps1 new file mode 100644 index 000000000000..e4ba018b2bfe --- /dev/null +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoInboundConnector.ps1 @@ -0,0 +1,32 @@ +function Set-CIPPDBCacheExoInboundConnector { + <# + .SYNOPSIS + Caches Exchange Online inbound connectors + + .PARAMETER TenantFilter + The tenant to cache inbound connector data for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter, + [string]$QueueId + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange inbound connectors' -sev Debug + + $InboundConnectors = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-InboundConnector' + if ($InboundConnectors) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoInboundConnector' -Data $InboundConnectors -AddCount + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($InboundConnectors.Count) inbound connectors" -sev Debug + } + $InboundConnectors = $null + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache inbound connector data: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA233_1.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA233_1.ps1 index 3fb1c2970d87..e195ceb45eea 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA233_1.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA233_1.ps1 @@ -6,39 +6,87 @@ function Invoke-CippTestORCA233_1 { param($Tenant) try { - $OrgConfig = Get-CIPPTestData -TenantFilter $Tenant -Type 'ExoOrganizationConfig' + $Connectors = Get-CIPPTestData -TenantFilter $Tenant -Type 'ExoInboundConnector' - if (-not $OrgConfig) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA233_1' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No organization config found in database.' -Risk 'Medium' -Name 'Enhanced filtering on default connectors' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Configuration' + if (-not $Connectors) { + # No connectors at all means no third-party mail flow path to misconfigure. + $Result = [System.Text.StringBuilder]::new("No inbound connectors are configured. Enhanced filtering is not required.") + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA233_1' -TestType 'Identity' -Status 'Passed' -ResultMarkdown $Result -Risk 'Medium' -Name 'Enhanced filtering on default connectors' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Connectors' return } - $Config = $OrgConfig | Select-Object -First 1 + # Find enabled connectors with wildcard sender domain ('smtp:*;N' priority pattern). + # These are the third-party mail flow connectors that need enhanced filtering. + $WildcardPattern = '^smtp:\*;(\d+)$' + $RelevantConnectors = [System.Collections.Generic.List[object]]::new() + foreach ($Connector in $Connectors) { + if ($Connector.Enabled -ne $true) { continue } + foreach ($SenderDomain in @($Connector.SenderDomains)) { + if ($SenderDomain -match $WildcardPattern) { + $RelevantConnectors.Add($Connector) | Out-Null + break + } + } + } + + if ($RelevantConnectors.Count -eq 0) { + $Result = [System.Text.StringBuilder]::new("No enabled inbound connectors with wildcard sender domains were found. Enhanced filtering is not required.") + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA233_1' -TestType 'Identity' -Status 'Passed' -ResultMarkdown $Result -Risk 'Medium' -Name 'Enhanced filtering on default connectors' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Connectors' + return + } + + $FailedConnectors = [System.Collections.Generic.List[object]]::new() + $PassedConnectors = [System.Collections.Generic.List[object]]::new() + + foreach ($Connector in $RelevantConnectors) { + $SkipLast = $Connector.EFSkipLastIP -eq $true + $SkipIPsCount = @($Connector.EFSkipIPs).Count + $TestMode = $Connector.EFTestMode -eq $true + $UsersCount = @($Connector.EFUsers).Count + + $IsCompliant = ($SkipLast -or $SkipIPsCount -gt 0) -and -not $TestMode -and $UsersCount -eq 0 + + $Mode = if ($SkipLast) { 'Last IP' } + elseif ($SkipIPsCount -gt 0) { "Skip IPs ($SkipIPsCount)" } + else { 'Not Configured' } + if ($TestMode) { $Mode += ' (Test Mode)' } + if ($UsersCount -gt 0) { $Mode += " (Select Users: $UsersCount)" } - # Check if enhanced filtering is enabled - # This property may vary depending on Exchange Online version - $EnhancedFilteringEnabled = $false + $Entry = [PSCustomObject]@{ + Identity = $Connector.Identity + Mode = $Mode + } - # Check various properties that indicate enhanced filtering - if ($Config.PSObject.Properties.Name -contains 'SkipListedFromForging') { - $EnhancedFilteringEnabled = $Config.SkipListedFromForging -eq $false + if ($IsCompliant) { $PassedConnectors.Add($Entry) | Out-Null } + else { $FailedConnectors.Add($Entry) | Out-Null } } - if ($EnhancedFilteringEnabled) { + if ($FailedConnectors.Count -eq 0) { $Status = 'Passed' - $Result = [System.Text.StringBuilder]::new("Enhanced filtering appears to be properly configured.`n`n") - $null = $Result.Append("**Configuration:** Reviewed") + $Result = [System.Text.StringBuilder]::new("All inbound connectors with wildcard sender domains have enhanced filtering configured.`n`n") + $null = $Result.Append("**Compliant Connectors:** $($PassedConnectors.Count)`n`n") + $null = $Result.Append("| Connector | EF Mode |`n") + $null = $Result.Append("|-----------|---------|`n") + foreach ($Entry in $PassedConnectors) { + $null = $Result.Append("| $($Entry.Identity) | $($Entry.Mode) |`n") + } } else { - $Status = 'Informational' - $Result = [System.Text.StringBuilder]::new("Unable to fully determine enhanced filtering status. Manual review recommended.`n`n") - $null = $Result.Append("**Action Required:** Review inbound connectors for enhanced filtering configuration") + $Status = 'Failed' + $Result = [System.Text.StringBuilder]::new("$($FailedConnectors.Count) inbound connectors do not have enhanced filtering configured correctly.`n`n") + $null = $Result.Append("**Failed:** $($FailedConnectors.Count) | **Passed:** $($PassedConnectors.Count)`n`n") + $null = $Result.Append("| Connector | EF Mode |`n") + $null = $Result.Append("|-----------|---------|`n") + foreach ($Entry in $FailedConnectors) { + $null = $Result.Append("| $($Entry.Identity) | $($Entry.Mode) |`n") + } + $null = $Result.Append("`n**Remediation:** Enable enhanced filtering on each connector by setting `EFSkipLastIP = `$true` (or populating `EFSkipIPs`), with `EFTestMode = `$false` and no per-user scoping.") } - Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA233_1' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Enhanced filtering on default connectors' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Configuration' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA233_1' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Enhanced filtering on default connectors' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Connectors' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA233_1' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Enhanced filtering on default connectors' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Configuration' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA233_1' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Enhanced filtering on default connectors' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Connectors' } } From 28ba94b43709b89dee2a349742f68c1df5ac1bf8 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 11 Jun 2026 01:17:33 +0800 Subject: [PATCH 196/202] Fixes ORCA242 --- Config/CIPPDBCacheTypes.json | 5 ++ .../Public/Invoke-CIPPDBCacheCollection.ps1 | 1 + .../Set-CIPPDBCacheExoProtectionAlert.ps1 | 36 +++++++++ .../ORCA/Identity/Invoke-CippTestORCA242.ps1 | 75 +++++++++++++++---- 4 files changed, 104 insertions(+), 13 deletions(-) create mode 100644 Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoProtectionAlert.ps1 diff --git a/Config/CIPPDBCacheTypes.json b/Config/CIPPDBCacheTypes.json index 7ee7d80240ed..003fb3457dd1 100644 --- a/Config/CIPPDBCacheTypes.json +++ b/Config/CIPPDBCacheTypes.json @@ -249,6 +249,11 @@ "friendlyName": "Exchange Inbound Connectors", "description": "Exchange Online inbound connectors (includes enhanced filtering settings)" }, + { + "type": "ExoProtectionAlert", + "friendlyName": "Exchange Protection Alerts", + "description": "Microsoft 365 protection alert policies (Security & Compliance endpoint)" + }, { "type": "Mailboxes", "friendlyName": "Mailboxes", diff --git a/Modules/CIPPCore/Public/Invoke-CIPPDBCacheCollection.ps1 b/Modules/CIPPCore/Public/Invoke-CIPPDBCacheCollection.ps1 index 13539fbfbe1b..06f258ec80f4 100644 --- a/Modules/CIPPCore/Public/Invoke-CIPPDBCacheCollection.ps1 +++ b/Modules/CIPPCore/Public/Invoke-CIPPDBCacheCollection.ps1 @@ -88,6 +88,7 @@ function Invoke-CIPPDBCacheCollection { 'ExoPresetSecurityPolicy' 'ExoTenantAllowBlockList' 'ExoInboundConnector' + 'ExoProtectionAlert' 'OwaMailboxPolicy' 'ReportSubmissionPolicy' 'ExoTransportConfig' diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoProtectionAlert.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoProtectionAlert.ps1 new file mode 100644 index 000000000000..f0cb8cfcf8fc --- /dev/null +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoProtectionAlert.ps1 @@ -0,0 +1,36 @@ +function Set-CIPPDBCacheExoProtectionAlert { + <# + .SYNOPSIS + Caches Exchange Online / Purview protection alert policies + + .DESCRIPTION + Calls Get-ProtectionAlert via the Security & Compliance PowerShell endpoint + (requires the -Compliance switch on New-ExoRequest). + + .PARAMETER TenantFilter + The tenant to cache protection alert data for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter, + [string]$QueueId + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange protection alerts' -sev Debug + + $ProtectionAlerts = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-ProtectionAlert' -Compliance + if ($ProtectionAlerts) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoProtectionAlert' -Data $ProtectionAlerts -AddCount + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($ProtectionAlerts.Count) protection alerts" -sev Debug + } + $ProtectionAlerts = $null + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache protection alert data: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA242.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA242.ps1 index b2e15b06483a..1486fffa483a 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA242.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA242.ps1 @@ -6,19 +6,68 @@ function Invoke-CippTestORCA242 { param($Tenant) try { - # This test would check for alert policies related to ATP/Defender for Office 365 - # Since we don't have an alert policy cache, we'll provide informational guidance - - $Status = 'Informational' - $Result = [System.Text.StringBuilder]::new("Alert policies for protection features should be enabled and monitored.`n`n") - $null = $Result.Append("**Recommended Alert Policies:**`n`n") - $null = $Result.Append("- Messages reported by users as malware or phish`n") - $null = $Result.Append("- Email sending limit exceeded`n") - $null = $Result.Append("- Suspicious email forwarding activity`n") - $null = $Result.Append("- Malware campaign detected`n") - $null = $Result.Append("- Suspicious connector activity`n") - $null = $Result.Append("- Unusual external user file activity`n") - $null = $Result.Append("`n**Action Required:** Verify alert policies are configured in Microsoft 365 Security & Compliance Center") + $Alerts = Get-CIPPTestData -TenantFilter $Tenant -Type 'ExoProtectionAlert' + + if (-not $Alerts) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA242' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No protection alert data found. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Important protection alerts enabled' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Configuration' + return + } + + # ORCA-242: alerts that drive Automated Incident Response (AIR). + # Alerts not present in the tenant are skipped (Microsoft hasn't deployed them). + $ImportantAlerts = @( + 'A potentially malicious URL click was detected' + 'Teams message reported by user as security risk' + 'Email messages containing phish URLs removed after delivery' + 'Suspicious Email Forwarding Activity' + 'Malware not zapped because ZAP is disabled' + 'Phish delivered due to an ETR override' + 'Email messages containing malicious file removed after delivery' + 'Email reported by user as malware or phish' + 'Email messages containing malicious URL removed after delivery' + 'Email messages containing malware removed after delivery' + 'A user clicked through to a potentially malicious URL' + 'Email messages from a campaign removed after delivery' + 'Email messages removed after delivery' + 'Suspicious email sending patterns detected' + ) + + $FailedAlerts = [System.Collections.Generic.List[object]]::new() + $PassedAlerts = [System.Collections.Generic.List[object]]::new() + + foreach ($AlertName in $ImportantAlerts) { + $Found = $Alerts | Where-Object { $_.Name -eq $AlertName } | Select-Object -First 1 + if ($null -eq $Found) { continue } + + if ($Found.Disabled -eq $true) { + $FailedAlerts.Add($Found) | Out-Null + } else { + $PassedAlerts.Add($Found) | Out-Null + } + } + + if ($FailedAlerts.Count -eq 0 -and $PassedAlerts.Count -eq 0) { + $Status = 'Skipped' + $Result = [System.Text.StringBuilder]::new('None of the AIR-related protection alerts are deployed to this tenant. This may indicate missing Defender for Office 365 licensing.') + } elseif ($FailedAlerts.Count -eq 0) { + $Status = 'Passed' + $Result = [System.Text.StringBuilder]::new("All AIR-related protection alerts deployed to this tenant are enabled.`n`n") + $null = $Result.Append("**Enabled Alerts:** $($PassedAlerts.Count)`n`n") + $null = $Result.Append("| Alert Name |`n|------------|`n") + foreach ($Alert in $PassedAlerts) { + $null = $Result.Append("| $($Alert.Name) |`n") + } + } else { + $Status = 'Failed' + $Result = [System.Text.StringBuilder]::new("$($FailedAlerts.Count) AIR-related protection alerts are disabled.`n`n") + $null = $Result.Append("**Disabled:** $($FailedAlerts.Count) | **Enabled:** $($PassedAlerts.Count)`n`n") + $null = $Result.Append("### Disabled Alerts`n`n") + $null = $Result.Append("| Alert Name | Disabled |`n|------------|----------|`n") + foreach ($Alert in $FailedAlerts) { + $null = $Result.Append("| $($Alert.Name) | $($Alert.Disabled) |`n") + } + $null = $Result.Append("`n**Remediation:** Re-enable these alert policies. Automated Incident Response (AIR) triggers from them and cannot function correctly when they are disabled.") + } Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA242' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Important protection alerts enabled' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Configuration' From f37d68adfa17516f05cb9422fe01da589252ada9 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 11 Jun 2026 01:26:09 +0800 Subject: [PATCH 197/202] Fixes ORCA235 --- .../ORCA/Identity/Invoke-CippTestORCA235.ps1 | 54 +++++++++++++------ 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA235.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA235.ps1 index ae123a59de2c..cfbbd0491932 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA235.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA235.ps1 @@ -6,31 +6,53 @@ function Invoke-CippTestORCA235 { param($Tenant) try { - $AcceptedDomains = Get-CIPPTestData -TenantFilter $Tenant -Type 'ExoAcceptedDomains' + $Results = Get-CIPPDomainAnalyser -TenantFilter $Tenant - if (-not $AcceptedDomains) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA235' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No accepted domains found in database.' -Risk 'High' -Name 'SPF records setup for custom domains' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Configuration' + if (-not $Results) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA235' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No Domain Analyser results found for this tenant. Run the CIPP Domain Analyser to populate domain health data.' -Risk 'High' -Name 'SPF records setup for custom domains' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Configuration' return } - # Note: This test would ideally check DNS SPF records - # Since we don't have DNS query capability here, we'll provide informational guidance + # ORCA scopes this to custom domains; onmicrosoft.com is handled by Microsoft. + $CustomDomains = $Results | Where-Object { $_.Domain -notlike '*.onmicrosoft.com' } - $CustomDomains = $AcceptedDomains | Where-Object { $_.DomainName -notlike '*.onmicrosoft.com' } + if (-not $CustomDomains -or $CustomDomains.Count -eq 0) { + $Status = 'Passed' + $Result = [System.Text.StringBuilder]::new('No custom domains found. Only onmicrosoft.com in use.') + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA235' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'SPF records setup for custom domains' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Configuration' + return + } + + $FailedDomains = [System.Collections.Generic.List[object]]::new() + $PassedDomains = [System.Collections.Generic.List[object]]::new() + + foreach ($Domain in $CustomDomains) { + # Valid SPF must start with v=spf1 AND end with -all (hard fail). + # ~all (soft fail), ?all (neutral), and +all (pass all) are insufficient. + # Third-party includes/IPs (Mimecast/Proofpoint/marketing) are fine alongside -all. + $Spf = [string]$Domain.ActualSPFRecord + $HasSpf = $Spf -match 'v=spf1' + $HasHardFail = $Spf -match '-all\s*$' + + if ($HasSpf -and $HasHardFail) { + $PassedDomains.Add($Domain) | Out-Null + } else { + $FailedDomains.Add($Domain) | Out-Null + } + } - if ($CustomDomains.Count -eq 0) { + if ($FailedDomains.Count -eq 0) { $Status = 'Passed' - $Result = [System.Text.StringBuilder]::new("No custom domains found. Only using onmicrosoft.com domain.`n`n") - $null = $Result.Append("**Total Domains:** $($AcceptedDomains.Count)") + $Result = [System.Text.StringBuilder]::new("All $($PassedDomains.Count) custom domains have a valid SPF record ending in -all.") } else { - $Status = 'Informational' - $Result = [System.Text.StringBuilder]::new("Found $($CustomDomains.Count) custom domains that should have SPF records configured.`n`n") - $null = $Result.Append("**Custom Domains:**`n`n") - foreach ($Domain in $CustomDomains) { - $null = $Result.Append("- $($Domain.DomainName)`n") + $Status = 'Failed' + $Result = [System.Text.StringBuilder]::new("$($FailedDomains.Count) of $($CustomDomains.Count) custom domains are missing a valid SPF record or do not end in -all (hard fail).`n`n") + $null = $Result.Append("| Domain | SPF Record |`n| :----- | :--------- |`n") + foreach ($Domain in ($FailedDomains | Select-Object -First 25)) { + $Display = if ([string]::IsNullOrWhiteSpace($Domain.ActualSPFRecord)) { '*(none)*' } else { $Domain.ActualSPFRecord } + $null = $Result.Append("| $($Domain.Domain) | $Display |`n") } - $null = $Result.Append("`n**Action Required:** Verify that each custom domain has an SPF record including Microsoft 365:`n") - $null = $Result.Append("``v=spf1 include:spf.protection.outlook.com -all``") + $null = $Result.Append("`n**Remediation:** Publish an SPF TXT record ending in `-all` (hard fail). For Microsoft 365 only: `v=spf1 include:spf.protection.outlook.com -all`. If routing through a third-party gateway, include that provider alongside (e.g. Mimecast, Proofpoint, marketing services), but keep `-all` at the end. Avoid `~all`, `?all`, and especially `+all`.") } Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA235' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'SPF records setup for custom domains' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Configuration' From e9bc5ad50bce44db439e9d271c37a0ec5b47a5ac Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 10 Jun 2026 16:51:47 -0400 Subject: [PATCH 198/202] fix: add early template filter when supplied improve performance and accuracy of applied templates --- .../Public/Standards/Get-CIPPStandards.ps1 | 80 ++++++++++--------- 1 file changed, 43 insertions(+), 37 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Get-CIPPStandards.ps1 b/Modules/CIPPCore/Public/Standards/Get-CIPPStandards.ps1 index 4b3dea9eda89..6a86545741f5 100644 --- a/Modules/CIPPCore/Public/Standards/Get-CIPPStandards.ps1 +++ b/Modules/CIPPCore/Public/Standards/Get-CIPPStandards.ps1 @@ -26,15 +26,21 @@ function Get-CIPPStandards { # can compute correct precedence. The $TemplateId filter is applied after merge so that # manual runs of a single template don't bypass tenant-specific overrides. $Templates = (Get-CIPPAzDataTableEntity @Table -Filter $Filter | Sort-Object TimeStamp).JSON | - ForEach-Object { - try { - # Fix old "Action" => "action" - $JSON = $_ -replace '"Action":', '"action":' -replace '"permissionlevel":', '"permissionLevel":' - ConvertFrom-Json -InputObject $JSON -ErrorAction SilentlyContinue - } catch {} - } | - Where-Object { - $_.runManually -eq $runManually + ForEach-Object { + try { + # Fix old "Action" => "action" + $JSON = $_ -replace '"Action":', '"action":' -replace '"permissionlevel":', '"permissionLevel":' + ConvertFrom-Json -InputObject $JSON -ErrorAction SilentlyContinue + } catch {} + } | + Where-Object { + $_.runManually -eq $runManually + } + + if ($TemplateId -ne '*' -and ![string]::IsNullOrEmpty($TemplateId)) { + $Templates = $Templates | Where-Object { + $_.GUID -like $TemplateId + } } # 1.5. Expand templates that contain TemplateList-Tags into multiple standards @@ -50,35 +56,35 @@ function Get-CIPPStandards { if ($IsArray) { $NewArray = @(foreach ($Item in $StandardValue) { - if ($Item.'TemplateList-Tags'.value) { - $HasExpansions = $true - $Table = Get-CippTable -tablename 'templates' - $PartitionKey = switch ($StandardName) { - 'ConditionalAccessTemplate' { 'CATemplate' } - 'IntuneTemplate' { 'IntuneTemplate' } - default { 'IntuneTemplate' } - } - $Filter = "PartitionKey eq '$PartitionKey'" - $TemplatesList = Get-CIPPAzDataTableEntity @Table -Filter $Filter | Where-Object -Property package -EQ $Item.'TemplateList-Tags'.value - Write-Information "Expanding $StandardName tag '$($Item.'TemplateList-Tags'.value)' from partition '$PartitionKey': found $(@($TemplatesList).Count) templates" - - foreach ($TemplateItem in $TemplatesList) { - $TemplateJSON = $TemplateItem.JSON | ConvertFrom-Json -Depth 100 -ErrorAction SilentlyContinue - $TemplateLabel = if ($TemplateJSON.displayName) { $TemplateJSON.displayName } else { "$($TemplateItem.RowKey)" } - $NewItem = $Item.PSObject.Copy() - $NewItem.PSObject.Properties.Remove('TemplateList-Tags') - $NewItem | Add-Member -NotePropertyName TemplateList -NotePropertyValue ([pscustomobject]@{ - label = $TemplateLabel - value = "$($TemplateItem.RowKey)" - }) -Force - $NewItem | Add-Member -NotePropertyName TemplateId -NotePropertyValue $Template.GUID -Force - $NewItem + if ($Item.'TemplateList-Tags'.value) { + $HasExpansions = $true + $Table = Get-CippTable -tablename 'templates' + $PartitionKey = switch ($StandardName) { + 'ConditionalAccessTemplate' { 'CATemplate' } + 'IntuneTemplate' { 'IntuneTemplate' } + default { 'IntuneTemplate' } + } + $Filter = "PartitionKey eq '$PartitionKey'" + $TemplatesList = Get-CIPPAzDataTableEntity @Table -Filter $Filter | Where-Object -Property package -EQ $Item.'TemplateList-Tags'.value + Write-Information "Expanding $StandardName tag '$($Item.'TemplateList-Tags'.value)' from partition '$PartitionKey': found $(@($TemplatesList).Count) templates" + + foreach ($TemplateItem in $TemplatesList) { + $TemplateJSON = $TemplateItem.JSON | ConvertFrom-Json -Depth 100 -ErrorAction SilentlyContinue + $TemplateLabel = if ($TemplateJSON.displayName) { $TemplateJSON.displayName } else { "$($TemplateItem.RowKey)" } + $NewItem = $Item.PSObject.Copy() + $NewItem.PSObject.Properties.Remove('TemplateList-Tags') + $NewItem | Add-Member -NotePropertyName TemplateList -NotePropertyValue ([pscustomobject]@{ + label = $TemplateLabel + value = "$($TemplateItem.RowKey)" + }) -Force + $NewItem | Add-Member -NotePropertyName TemplateId -NotePropertyValue $Template.GUID -Force + $NewItem + } + } else { + $Item | Add-Member -NotePropertyName TemplateId -NotePropertyValue $Template.GUID -Force + $Item } - } else { - $Item | Add-Member -NotePropertyName TemplateId -NotePropertyValue $Template.GUID -Force - $Item - } - }) + }) if ($NewArray.Count -gt 0) { $ExpandedStandards[$StandardName] = $NewArray } From 8f2198e7cf303ec2726066c95a4ebacb55e75f27 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 10 Jun 2026 17:02:38 -0400 Subject: [PATCH 199/202] chore: bump version to 10.5.2 --- host.json | 2 +- version_latest.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/host.json b/host.json index accd9b7c83bc..7648204274b0 100644 --- a/host.json +++ b/host.json @@ -16,7 +16,7 @@ "distributedTracingEnabled": false, "version": "None" }, - "defaultVersion": "10.5.1", + "defaultVersion": "10.5.2", "versionMatchStrategy": "Strict", "versionFailureStrategy": "Fail" } diff --git a/version_latest.txt b/version_latest.txt index 4a6e70e959e5..a39233be07ad 100644 --- a/version_latest.txt +++ b/version_latest.txt @@ -1 +1 @@ -10.5.1 +10.5.2 From b75c1fe90ceeb4ceec78b95b92e8314cd21c19d7 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 11 Jun 2026 13:35:04 +0800 Subject: [PATCH 200/202] move file --- .../{ => Entrypoints/HTTP Functions}/Invoke-ListObjectHistory.ps1 | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Modules/CIPPHTTP/Public/{ => Entrypoints/HTTP Functions}/Invoke-ListObjectHistory.ps1 (100%) diff --git a/Modules/CIPPHTTP/Public/Invoke-ListObjectHistory.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListObjectHistory.ps1 similarity index 100% rename from Modules/CIPPHTTP/Public/Invoke-ListObjectHistory.ps1 rename to Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListObjectHistory.ps1 From 00597e40a0e3a2ded828127f7482954aa9c009ac Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 11 Jun 2026 14:08:03 +0800 Subject: [PATCH 201/202] Update Initialize-CIPPAuth.ps1 --- .../Public/Authentication/Initialize-CIPPAuth.ps1 | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Modules/CIPPCore/Public/Authentication/Initialize-CIPPAuth.ps1 b/Modules/CIPPCore/Public/Authentication/Initialize-CIPPAuth.ps1 index 78319c3a05b8..1382e009507a 100644 --- a/Modules/CIPPCore/Public/Authentication/Initialize-CIPPAuth.ps1 +++ b/Modules/CIPPCore/Public/Authentication/Initialize-CIPPAuth.ps1 @@ -145,6 +145,18 @@ function Initialize-CIPPAuth { Write-Information "[Auth-Init] EasyAuth issuer reconciliation failed (non-fatal): $_" } } + + # 3c. Reconcile EasyAuth policy (UnauthenticatedClientAction, ExcludedPaths) with appsettings configuration + if ($AuthState.HasSAMCredentials -and -not $env:CIPP_SSO_MIGRATION_APPID) { + try { + $PolicyReconciled = [Craft.Services.AppLifecycleBridge]::ReconcileAuthPolicy('CIPP warmup') + if ($PolicyReconciled) { + Write-Information '[Auth-Init] EasyAuth policy reconciled from Craft appsettings (drift detected and corrected)' + } + } catch { + Write-Information "[Auth-Init] EasyAuth policy reconcile failed (non-fatal): $_" + } + } } elseif ($AuthState.HasSAMCredentials) { # EasyAuth NOT configured but we DO have SAM credentials — try to auto-configure Write-Information '[Auth-Init] EasyAuth not configured but SAM credentials available — attempting auto-configuration' From 3cd0b3f5c35525ffea074f7cf1c9b3e7aba2e5c9 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 11 Jun 2026 14:38:11 +0800 Subject: [PATCH 202/202] Update Initialize-CIPPAuth.ps1 --- Modules/CIPPCore/Public/Authentication/Initialize-CIPPAuth.ps1 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Modules/CIPPCore/Public/Authentication/Initialize-CIPPAuth.ps1 b/Modules/CIPPCore/Public/Authentication/Initialize-CIPPAuth.ps1 index 1382e009507a..ec5c613b4692 100644 --- a/Modules/CIPPCore/Public/Authentication/Initialize-CIPPAuth.ps1 +++ b/Modules/CIPPCore/Public/Authentication/Initialize-CIPPAuth.ps1 @@ -152,6 +152,8 @@ function Initialize-CIPPAuth { $PolicyReconciled = [Craft.Services.AppLifecycleBridge]::ReconcileAuthPolicy('CIPP warmup') if ($PolicyReconciled) { Write-Information '[Auth-Init] EasyAuth policy reconciled from Craft appsettings (drift detected and corrected)' + } else { + Write-Information '[Auth-Init] EasyAuth policy matches appsettings — no update needed' } } catch { Write-Information "[Auth-Init] EasyAuth policy reconcile failed (non-fatal): $_"