diff --git a/.github/agents/CIPP-Alert-Agent.md b/.github/agents/CIPP-Alert-Agent.md index b698cfef4eb5..b2aa3e90c887 100644 --- a/.github/agents/CIPP-Alert-Agent.md +++ b/.github/agents/CIPP-Alert-Agent.md @@ -102,16 +102,13 @@ When adding or modifying alerts: When an alert depends on a tenant having certain SKUs or capabilities, you **must**: -- Use `Test-CIPPStandardLicense` +- Use `Test-CIPPStandardLicense` +- Prefer `-Preset` for common capability sets: `Exchange`, `SharePoint`, `Intune`, `Entra`, `EntraP2`, `Teams`, `Compliance` +- Use `-RequiredCapabilities` only when no preset matches, or combine it with `-Preset` for extra edge-case capabilities - Do **not** manually inspect SKUs, raw license IDs, or raw capability lists. Example pattern (adapt to the specific feature): ```powershell -$TestResult = Test-CIPPStandardLicense -StandardName 'AutopilotProfile' -TenantFilter $Tenant -RequiredCapabilities @( - 'INTUNE_A', - 'MDM_Services', - 'EMS', - 'SCCM', - 'MICROSOFTINTUNEPLAN1' -) +$TestResult = Test-CIPPStandardLicense -StandardName 'AutopilotProfile' -TenantFilter $Tenant -Preset Intune +``` diff --git a/.github/agents/CIPP-Standards-Agent.md b/.github/agents/CIPP-Standards-Agent.md index ed5529cefe5c..506a3ee4edb8 100644 --- a/.github/agents/CIPP-Standards-Agent.md +++ b/.github/agents/CIPP-Standards-Agent.md @@ -82,6 +82,7 @@ When adding or modifying standards: - Similar logging and error handling - Reuse helper functions instead of inlining raw Graph calls or custom HTTP code. - Keep behaviour predictable. +- If a standard needs license gating, use `Test-CIPPStandardLicense` with `-Preset` for common capability sets (`Exchange`, `SharePoint`, `Intune`, `Entra`, `EntraP2`, `Teams`, `Compliance`). Use `-RequiredCapabilities` only when no preset matches, or combine it with `-Preset` for extra edge-case capabilities. ### 2. Return the code for the frontend. diff --git a/.github/instructions/alerts.instructions.md b/.github/instructions/alerts.instructions.md index c5633d083528..a05c31e3721e 100644 --- a/.github/instructions/alerts.instructions.md +++ b/.github/instructions/alerts.instructions.md @@ -97,14 +97,11 @@ if ($InputValue -is [string] -and $InputValue.Trim().StartsWith('{')) { If the alert depends on a specific M365 capability (Intune, Exchange, Defender, etc.), gate it early with `Test-CIPPStandardLicense`. Never inspect raw SKU IDs manually. ```powershell -$Licensed = Test-CIPPStandardLicense -StandardName '' -TenantFilter $TenantFilter -RequiredCapabilities @( - 'INTUNE_A', - 'MDM_Services' -) +$Licensed = Test-CIPPStandardLicense -StandardName '' -TenantFilter $TenantFilter -Preset Intune if (-not $Licensed) { return } ``` -Reference existing alerts in the same domain for common capability strings. The `Test-CIPPStandardLicense` function source documents the capability matching logic. +Use presets for common service families: `Exchange`, `SharePoint`, `Intune`, `Entra`, `EntraP2`, `Teams`, and `Compliance`. Use `-RequiredCapabilities` only when no preset matches, or combine it with `-Preset` when an alert needs a preset plus extra edge-case capabilities. ## Querying data diff --git a/.github/instructions/cippdb.instructions.md b/.github/instructions/cippdb.instructions.md index 19d31e78dcdd..fab2c73c1dad 100644 --- a/.github/instructions/cippdb.instructions.md +++ b/.github/instructions/cippdb.instructions.md @@ -141,7 +141,7 @@ function Set-CIPPDBCacheMyNewType { try { # 1. Optional license check - $Licensed = Test-CIPPStandardLicense -StandardName 'MyFeature' -TenantFilter $TenantFilter -RequiredCapabilities @('REQUIRED_SKU') + $Licensed = Test-CIPPStandardLicense -StandardName 'MyFeature' -TenantFilter $TenantFilter -Preset Intune if (-not $Licensed) { return } # 2. Fetch data from API @@ -160,7 +160,7 @@ function Set-CIPPDBCacheMyNewType { - **Always use `-AddCount`** unless you handle count rows manually - **Pipeline streaming** for large datasets: pipe directly from `New-GraphGetRequest` into `Add-CIPPDbItem` -- **License gating**: use `Test-CIPPStandardLicense` when the API requires specific SKUs +- **License gating**: use `Test-CIPPStandardLicense -Preset ` for common capability sets (`Exchange`, `SharePoint`, `Intune`, `Entra`, `EntraP2`, `Teams`, `Compliance`); use `-RequiredCapabilities` only for non-preset capabilities or additional edge-case capabilities - **Conditional `$select`**: expand Graph `$select` fields based on license capabilities - **Error handling**: catch, log with `Write-LogMessage`, do not rethrow (allows other types in the collection to continue) - **No explicit return** of data — these functions write to the table as a side effect diff --git a/.github/instructions/standards.instructions.md b/.github/instructions/standards.instructions.md index 113a6bbaadb8..7363a4f936f9 100644 --- a/.github/instructions/standards.instructions.md +++ b/.github/instructions/standards.instructions.md @@ -67,7 +67,7 @@ function Invoke-CIPPStandard { # 1. License gate (if the data source requires a specific SKU) $TestResult = Test-CIPPStandardLicense -StandardName '' -TenantFilter $Tenant ` - -RequiredCapabilities @('CAPABILITY_1', 'CAPABILITY_2') + -Preset Exchange if ($TestResult -eq $false) { return $true } # 2. Get current state @@ -235,13 +235,13 @@ Gate early using `Test-CIPPStandardLicense`. Never inspect raw SKU IDs. ```powershell $TestResult = Test-CIPPStandardLicense -StandardName '' -TenantFilter $Tenant ` - -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE') + -Preset Exchange if ($TestResult -eq $false) { return $true } ``` The function checks tenant capabilities, logs if missing, and automatically sets the `Set-CIPPStandardsCompareField` with `LicenseAvailable = $false`. -Reference existing standards in the same domain for common capability strings. The `Test-CIPPStandardLicense` function source documents the capability matching logic. +Use presets for common service families: `Exchange`, `SharePoint`, `Intune`, `Entra`, `EntraP2`, `Teams`, and `Compliance`. Use `-RequiredCapabilities` only when no preset matches, or combine it with `-Preset` when a standard needs a preset plus extra edge-case capabilities. ## API call patterns @@ -337,7 +337,7 @@ The comment-based help `.NOTES` block drives the frontend UI. Each field maps to | `RECOMMENDEDBY` | `recommendedBy` | `"CIS"`, `"CIPP"`, etc. | | `MULTIPLE` | `multiple` | `True` for template-based standards (can have multiple instances) | | `DISABLEDFEATURES` | `disabledFeatures` | JSON object disabling specific action modes | -| `REQUIREDCAPABILITIES` | *(discovery only)* | One capability string per line; parsed for standards metadata/JSON generation. The explicit `Test-CIPPStandardLicense` call in the function body still performs the actual runtime license check. | +| `REQUIREDCAPABILITIES` | *(discovery only)* | One capability string per line; generated from `Test-CIPPStandardLicense -Preset` and/or `-RequiredCapabilities` for standards metadata/JSON generation. The explicit `Test-CIPPStandardLicense` call in the function body still performs the actual runtime license check. | | `UPDATECOMMENTBLOCK` | *(tooling only)* | Always include with the literal value `Run the Tools\Update-StandardsComments.ps1 script to update this comment block`. Signals the comment-update tooling to regenerate this block. | ### Valid CAT values diff --git a/Config/CIPPTimers.json b/Config/CIPPTimers.json index b9899dec6d4b..077f7d53fb0c 100644 --- a/Config/CIPPTimers.json +++ b/Config/CIPPTimers.json @@ -264,5 +264,14 @@ "RunOnProcessor": true, "TZOffset": true, "IsSystem": true + }, + { + "Id": "a3b4c5d6-e7f8-4a9b-8c1d-2e3f4a5b6c7d", + "Command": "Start-ContainerUpdateCheck", + "Description": "Check for container image updates and optionally auto-restart", + "Cron": "0 0 * * * *", + "Priority": 30, + "RunOnProcessor": false, + "IsSystem": true } ] diff --git a/Config/FeatureFlags.json b/Config/FeatureFlags.json index 61a49d453458..35c51a44e865 100644 --- a/Config/FeatureFlags.json +++ b/Config/FeatureFlags.json @@ -19,6 +19,31 @@ "/tenant/standards/bpa-report", "/tenant/standards/bpa-report/builder", "/tenant/standards/bpa-report/view" - ] + ], + "Hidden": false + }, + { + "Id": "SuperAdminNG", + "Name": "Super Admin", + "Description": "Additional super admin pages for CIPP instances (CIPP Users, SSO, Container management).", + "Enabled": false, + "AllowUserToggle": false, + "Timers": [], + "Endpoints": [ + "ExecCIPPUsers", + "ListCIPPUsers", + "ExecSSOSetup", + "ExecContainerManagement", + "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/worker-health" + ], + "Hidden": true } ] diff --git a/Config/SAMManifest.json b/Config/SAMManifest.json index 87e3978f2483..ab325ce85ae1 100644 --- a/Config/SAMManifest.json +++ b/Config/SAMManifest.json @@ -571,6 +571,10 @@ { "id": "a94a502d-0281-4d15-8cd2-682ac9362c4c", "type": "Role" + }, + { + "id": "d72bdbf4-a59b-405c-8b04-5995895819ac", + "type": "Role" } ] }, 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/Push-CIPPDBCacheData.ps1 b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 index 79cae558e007..625eff841975 100644 --- a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 +++ b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 @@ -27,7 +27,7 @@ function Push-CIPPDBCacheData { # Check tenant capabilities for license-specific features $IntuneCapable = $false try { - $IntuneCapable = Test-CIPPStandardLicense -StandardName 'IntuneLicenseCheck' -TenantFilter $TenantFilter -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') -SkipLog + $IntuneCapable = Test-CIPPStandardLicense -StandardName 'IntuneLicenseCheck' -TenantFilter $TenantFilter -Preset Intune -SkipLog } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Intune license check failed: $($_.Exception.Message)" -sev Warning -LogData $ErrorMessage @@ -35,7 +35,7 @@ function Push-CIPPDBCacheData { $ConditionalAccessCapable = $false try { - $ConditionalAccessCapable = Test-CIPPStandardLicense -StandardName 'ConditionalAccessLicenseCheck' -TenantFilter $TenantFilter -RequiredCapabilities @('AAD_PREMIUM', 'AAD_PREMIUM_P2') -SkipLog + $ConditionalAccessCapable = Test-CIPPStandardLicense -StandardName 'ConditionalAccessLicenseCheck' -TenantFilter $TenantFilter -Preset Entra -SkipLog } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Conditional Access license check failed: $($_.Exception.Message)" -sev Warning -LogData $ErrorMessage @@ -43,7 +43,7 @@ function Push-CIPPDBCacheData { $AzureADPremiumP2Capable = $false try { - $AzureADPremiumP2Capable = Test-CIPPStandardLicense -StandardName 'AzureADPremiumP2LicenseCheck' -TenantFilter $TenantFilter -RequiredCapabilities @('AAD_PREMIUM_P2') -SkipLog + $AzureADPremiumP2Capable = Test-CIPPStandardLicense -StandardName 'AzureADPremiumP2LicenseCheck' -TenantFilter $TenantFilter -Preset EntraP2 -SkipLog } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Azure AD Premium P2 license check failed: $($_.Exception.Message)" -sev Warning -LogData $ErrorMessage @@ -51,7 +51,7 @@ function Push-CIPPDBCacheData { $ExchangeCapable = $false try { - $ExchangeCapable = Test-CIPPStandardLicense -StandardName 'ExchangeLicenseCheck' -TenantFilter $TenantFilter -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') -SkipLog + $ExchangeCapable = Test-CIPPStandardLicense -StandardName 'ExchangeLicenseCheck' -TenantFilter $TenantFilter -Preset Exchange -SkipLog } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Exchange license check failed: $($_.Exception.Message)" -sev Warning -LogData $ErrorMessage @@ -59,7 +59,7 @@ function Push-CIPPDBCacheData { $ComplianceCapable = $false try { - $ComplianceCapable = Test-CIPPStandardLicense -StandardName 'ComplianceLicenseCheck' -TenantFilter $TenantFilter -RequiredCapabilities @('RMS_S_PREMIUM', 'RMS_S_PREMIUM2', 'MIP_S_CLP1', 'MIP_S_CLP2') -SkipLog + $ComplianceCapable = Test-CIPPStandardLicense -StandardName 'ComplianceLicenseCheck' -TenantFilter $TenantFilter -Preset Compliance -SkipLog } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Compliance license check failed: $($_.Exception.Message)" -sev Warning -LogData $ErrorMessage @@ -67,7 +67,7 @@ function Push-CIPPDBCacheData { $SharePointCapable = $false try { - $SharePointCapable = Test-CIPPStandardLicense -StandardName 'SharePointLicenseCheck' -TenantFilter $TenantFilter -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') -SkipLog + $SharePointCapable = Test-CIPPStandardLicense -StandardName 'SharePointLicenseCheck' -TenantFilter $TenantFilter -Preset SharePoint -SkipLog } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "SharePoint license check failed: $($_.Exception.Message)" -sev Warning -LogData $ErrorMessage @@ -75,7 +75,7 @@ function Push-CIPPDBCacheData { $TeamsCapable = $false try { - $TeamsCapable = Test-CIPPStandardLicense -StandardName 'TeamsLicenseCheck' -TenantFilter $TenantFilter -RequiredCapabilities @('MCOSTANDARD', 'MCOEV', 'MCOIMP', 'TEAMS1', 'Teams_Room_Standard') -SkipLog + $TeamsCapable = Test-CIPPStandardLicense -StandardName 'TeamsLicenseCheck' -TenantFilter $TenantFilter -Preset Teams -SkipLog } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Teams license check failed: $($_.Exception.Message)" -sev Warning -LogData $ErrorMessage diff --git a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-OrchestratorBatchItems.ps1 b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-OrchestratorBatchItems.ps1 index 9e8fe4561a1d..df59afa91619 100644 --- a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-OrchestratorBatchItems.ps1 +++ b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-OrchestratorBatchItems.ps1 @@ -11,8 +11,8 @@ function Push-OrchestratorBatchItems { $Entities = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq '$($Item.Parameters.BatchId)'" $BatchItems = [system.Collections.Generic.List[object]]::new() $Entities | ForEach-Object { - $Item = $_.BatchItem | ConvertFrom-Json - $BatchItems.Add($Item) + $BatchItem = $_.BatchItem | ConvertFrom-Json + $BatchItems.Add($BatchItem) } Write-Information "Retrieved $($BatchItems.Count) batch items for BatchId: $($Item.Parameters.BatchId)" } else { 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 e7df2cc7f723..1e52ae35375a 100644 --- a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandard.ps1 +++ b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandard.ps1 @@ -40,6 +40,12 @@ function Push-CIPPStandard { $StandardInfo.ConditionalAccessTemplateId = $Item.Settings.TemplateList.value } + if (-not (Get-Command -Name $FunctionName -Module CIPPStandards -ErrorAction SilentlyContinue)) { + Write-LogMessage -tenant $Tenant -message "The standard $Standard was not found. This may have been deprecated or replaced with a new standard." -sev 'Warning' -API 'Standards' + Write-Warning "Function $FunctionName not found" + return + } + # Initialize AsyncLocal storage for thread-safe per-invocation context # Uses $global: so Write-LogMessage (CIPPCore module) can read it across module boundaries if (-not $global:CippStandardInfoStorage) { 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 08c62d8d6863..e59f70ba4a02 100644 --- a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandardsList.ps1 +++ b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandardsList.ps1 @@ -38,7 +38,7 @@ function Push-CIPPStandardsList { if ($IntuneTemplateFound) { # Perform license check - $TestResult = Test-CIPPStandardLicense -StandardName 'IntuneTemplate_general' -TenantFilter $TenantFilter -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') + $TestResult = Test-CIPPStandardLicense -StandardName 'IntuneTemplate_general' -TenantFilter $TenantFilter -Preset Intune if (-not $TestResult) { # Remove IntuneTemplate standards and set compare fields @@ -249,7 +249,7 @@ function Push-CIPPStandardsList { $CAStandardFound = ($ComputedStandards.Keys.Where({ $_ -like '*ConditionalAccessTemplate*' }, 'First').Count -gt 0) if ($CAStandardFound) { - $TestResult = Test-CIPPStandardLicense -StandardName 'ConditionalAccessTemplate_general' -TenantFilter $TenantFilter -RequiredCapabilities @('AAD_PREMIUM', 'AAD_PREMIUM_P2') + $TestResult = Test-CIPPStandardLicense -StandardName 'ConditionalAccessTemplate_general' -TenantFilter $TenantFilter -Preset Entra if (-not $TestResult) { $CAKeys = @($ComputedStandards.Keys | Where-Object { $_ -like '*ConditionalAccessTemplate*' }) $BulkFields = [System.Collections.Generic.List[object]]::new() 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/CIPPAlerts/Public/Alerts/Get-CIPPAlertIntunePolicyConflicts.ps1 b/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertIntunePolicyConflicts.ps1 index 071f65cff4e7..99d2fb1ad5fe 100644 --- a/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertIntunePolicyConflicts.ps1 +++ b/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertIntunePolicyConflicts.ps1 @@ -57,14 +57,7 @@ function Get-CIPPAlertIntunePolicyConflicts { return } - $HasLicense = Test-CIPPStandardLicense -StandardName 'IntunePolicyStatus' -TenantFilter $TenantFilter -RequiredCapabilities @( - 'INTUNE_A', - 'MDM_Services', - 'EMS', - 'SCCM', - 'MICROSOFTINTUNEPLAN1' - ) - + $HasLicense = Test-CIPPStandardLicense -StandardName 'IntunePolicyStatus' -TenantFilter $TenantFilter -Preset Intune if (-not $HasLicense) { return } diff --git a/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertQuarantineReleaseRequests.ps1 b/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertQuarantineReleaseRequests.ps1 index 34e45a3cb853..cc6795457cf1 100644 --- a/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertQuarantineReleaseRequests.ps1 +++ b/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertQuarantineReleaseRequests.ps1 @@ -16,13 +16,7 @@ if ($Rerun) { return } - $HasLicense = Test-CIPPStandardLicense -StandardName 'QuarantineReleaseRequests' -TenantFilter $TenantFilter -RequiredCapabilities @( - 'EXCHANGE_S_STANDARD', - 'EXCHANGE_S_ENTERPRISE', - 'EXCHANGE_S_STANDARD_GOV', - 'EXCHANGE_S_ENTERPRISE_GOV', - 'EXCHANGE_LITE' - ) + $HasLicense = Test-CIPPStandardLicense -StandardName 'QuarantineReleaseRequests' -TenantFilter $TenantFilter -Preset Exchange if (-not $HasLicense) { return diff --git a/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertUserReportedPhishing.ps1 b/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertUserReportedPhishing.ps1 new file mode 100644 index 000000000000..60138097fc7b --- /dev/null +++ b/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertUserReportedPhishing.ps1 @@ -0,0 +1,47 @@ +function Get-CIPPAlertUserReportedPhishing { + <# + .FUNCTIONALITY + Entrypoint + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $false)] + [Alias('input')] + $InputValue, + $TenantFilter + ) + + try { + [int]$HoursBack = if ($InputValue.HoursBack) { [int]$InputValue.HoursBack } else { 24 } + $Since = (Get-Date).AddHours(-$HoursBack).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') + + $Submissions = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/security/threatSubmission/emailThreats?`$filter=createdDateTime ge $Since" -tenantid $TenantFilter -AsApp $true + + $AlertData = foreach ($Submission in $Submissions) { + # Only include user-reported submissions + if ($Submission.source -ne 'user') { continue } + + [PSCustomObject]@{ + ReportedBy = $Submission.createdBy.user.displayName + ReporterEmail = $Submission.createdBy.user.email + Sender = $Submission.sender + Subject = $Submission.emailSubject + Category = $Submission.category + ReceivedDateTime = $Submission.receivedDateTime + ReportedAt = $Submission.createdDateTime + Status = $Submission.status + ResultCategory = $Submission.result.category + ResultDetail = $Submission.result.detail + InternetMsgId = $Submission.internetMessageId + SubmissionId = $Submission.id + Tenant = $TenantFilter + } + } + if ($AlertData) { + Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData + } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-AlertMessage -message "User-reported phishing alert failed for $($TenantFilter): $($ErrorMessage.NormalizedError)" -tenant $TenantFilter -LogData $ErrorMessage + } +} diff --git a/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 b/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 index 60fcb1878353..6c2e1ee6f951 100644 --- a/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 @@ -40,6 +40,7 @@ function Add-CIPPAzDataTableEntity { $MaxRowSize = 500000 - 100 $MaxSize = 30kb $BatchQueue = [System.Collections.Generic.List[object]]::new() + $BatchKeys = [System.Collections.Generic.Dictionary[string,int]]::new() foreach ($SingleEnt in @($Entity)) { try { @@ -90,8 +91,14 @@ function Add-CIPPAzDataTableEntity { $entityBytes = [System.Text.Encoding]::UTF8.GetByteCount($($SingleEnt | ConvertTo-Json -Compress)) if ($entityBytes -lt $MaxSize) { - # Small entity - add to batch queue - $BatchQueue.Add($SingleEnt) + # Small entity - add to batch queue, dedup by PartitionKey+RowKey (last-in wins) + $batchKey = "$($SingleEnt.PartitionKey)|$($SingleEnt.RowKey)" + if ($BatchKeys.ContainsKey($batchKey)) { + $BatchQueue[$BatchKeys[$batchKey]] = $SingleEnt + } else { + $BatchKeys[$batchKey] = $BatchQueue.Count + $BatchQueue.Add($SingleEnt) + } if ($BatchQueue.Count -ge 100) { try { Add-AzDataTableEntity @Parameters -Entity $BatchQueue.ToArray() -ErrorAction Stop @@ -103,6 +110,7 @@ function Add-CIPPAzDataTableEntity { } } $BatchQueue.Clear() + $BatchKeys.Clear() } continue } @@ -118,6 +126,7 @@ function Add-CIPPAzDataTableEntity { } } $BatchQueue.Clear() + $BatchKeys.Clear() } Add-AzDataTableEntity @Parameters -Entity $SingleEnt -ErrorAction Stop @@ -284,5 +293,6 @@ function Add-CIPPAzDataTableEntity { } } $BatchQueue.Clear() + $BatchKeys.Clear() } } diff --git a/Modules/CIPPCore/Public/Authentication/Get-CippApiAuth.ps1 b/Modules/CIPPCore/Public/Authentication/Get-CippApiAuth.ps1 index 1c5880fd8be1..6d822746d6cf 100644 --- a/Modules/CIPPCore/Public/Authentication/Get-CippApiAuth.ps1 +++ b/Modules/CIPPCore/Public/Authentication/Get-CippApiAuth.ps1 @@ -4,27 +4,48 @@ function Get-CippApiAuth { [string]$FunctionAppName ) - $SubscriptionId = Get-CIPPAzFunctionAppSubId + $AuthSettings = $null - 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)" + # 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 + } + + # 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 -and $env:WEBSITE_AUTH_V2_CONFIG_JSON) { + # 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 }) + } + } + [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 + TenantID = $Issuer -replace 'https://sts.windows.net/', '' -replace 'https://login.microsoftonline.com/', '' -replace '/v2.0', '' + ClientIDs = $AllowedApps + Enabled = $AAD.enabled } } else { throw 'No auth settings found' diff --git a/Modules/CIPPCore/Public/Authentication/Initialize-CIPPAuth.ps1 b/Modules/CIPPCore/Public/Authentication/Initialize-CIPPAuth.ps1 index bf0b55902563..f5f48e325add 100644 --- a/Modules/CIPPCore/Public/Authentication/Initialize-CIPPAuth.ps1 +++ b/Modules/CIPPCore/Public/Authentication/Initialize-CIPPAuth.ps1 @@ -4,8 +4,10 @@ function Initialize-CIPPAuth { Bootstraps authentication state for CIPP. .DESCRIPTION - Loads SAM credentials from Key Vault (or DevSecrets table) - and auto-patches redirect URIs on the SAM app registration. + 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. #> [CmdletBinding()] param() @@ -41,7 +43,99 @@ function Initialize-CIPPAuth { try { Update-CIPPSAMRedirectUri } catch { - Write-Information "[Auth-Init] Redirect URI patch failed (non-fatal): $_" + Write-Information "[Auth-Init] SAM redirect URI patch failed (non-fatal): $_" + } + + try { + Update-CIPPSSORedirectUri + } catch { + Write-Information "[Auth-Init] SSO redirect URI patch failed (non-fatal): $_" + } + } + + # 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. + 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 { + $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') + } + } catch { + Write-Information "[Auth-Init] Implicit auth EasyAuth setup failed (non-fatal): $_" + } + return $AuthState + } + + Write-Information '[Auth-Init] EasyAuth not configured — checking for SSO credentials...' + try { + $SSOAppId = $null + $SSOMultiTenant = $false + + 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 + $SSOAppId = $Secret.SSOAppId + $SSOMultiTenant = $Secret.SSOMultiTenant -eq 'True' + } elseif ($KVName) { + try { $SSOAppId = Get-CippKeyVaultSecret -VaultName $KVName -Name 'SSOAppId' -AsPlainText -ErrorAction Stop } catch { } + try { + $mtVal = Get-CippKeyVaultSecret -VaultName $KVName -Name 'SSOMultiTenant' -AsPlainText -ErrorAction Stop + $SSOMultiTenant = $mtVal -eq 'True' + } catch { } + } + + if ($SSOAppId) { + Write-Information "[Auth-Init] Found SSO AppId ($SSOAppId) — configuring EasyAuth via ARM" + $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') + } + } 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') + } + } catch { + Write-Information "[Auth-Init] SSO EasyAuth setup failed (non-fatal): $_" + } + } + + # 5. 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): $_" } } diff --git a/Modules/CIPPCore/Public/Authentication/New-CIPPSSOApp.ps1 b/Modules/CIPPCore/Public/Authentication/New-CIPPSSOApp.ps1 new file mode 100644 index 000000000000..d8291eacd06b --- /dev/null +++ b/Modules/CIPPCore/Public/Authentication/New-CIPPSSOApp.ps1 @@ -0,0 +1,156 @@ +function New-CIPPSSOApp { + <# + .SYNOPSIS + Creates or updates the CIPP-SSO app registration for EasyAuth SSO migration. + .DESCRIPTION + 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. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$RedirectUri, + + [Parameter(Mandatory = $false)] + [bool]$MultiTenant = $false, + + [Parameter(Mandatory = $false)] + [string]$ExistingAppId + ) + + $AppDisplayName = 'CIPP-SSO' + $CallbackUri = $RedirectUri.TrimEnd('/') + '/.auth/login/aad/callback' + $SignInAudience = if ($MultiTenant) { 'AzureADMultipleOrgs' } else { 'AzureADMyOrg' } + + # Microsoft Graph resource ID and delegated permission GUIDs + $GraphResourceId = '00000003-0000-0000-c000-000000000000' + $Permissions = @( + @{ id = '37f7f235-527c-4136-accd-4a02d197296e'; type = 'Scope' } # openid + @{ id = '14dad69e-099b-42c9-810b-d002981feec1'; type = 'Scope' } # profile + @{ id = '64a6cdd6-aab1-4aaf-94b8-3cc8405e90d0'; type = 'Scope' } # email + ) + + # Look up existing app by stored AppId (not by name — supports multiple CIPP instances) + $ExistingApp = $null + if ($ExistingAppId) { + try { + $ExistingApp = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/applications(appId='$ExistingAppId')?`$select=id,appId,displayName,web" -NoAuthCheck $true -AsApp $true + Write-Information "[SSO-App] Found existing app by AppId: $ExistingAppId" + } catch { + Write-Information "[SSO-App] Stored AppId $ExistingAppId not found in tenant — will create new app" + } + } + + $AppObjectId = $null + $AppClientId = $null + $State = $null + + if ($ExistingApp) { + # Reuse existing app — patch redirect URIs and audience + $AppObjectId = $ExistingApp.id + $AppClientId = $ExistingApp.appId + $State = 'updated' + Write-Information "[SSO-App] Updating existing app: $AppClientId" + + $PatchBody = @{ + web = @{ + redirectUris = @($CallbackUri) + implicitGrantSettings = @{ enableIdTokenIssuance = $true } + } + signInAudience = $SignInAudience + requiredResourceAccess = @( + @{ + resourceAppId = $GraphResourceId + resourceAccess = $Permissions + } + ) + } | ConvertTo-Json -Depth 10 -Compress + + New-GraphPOSTRequest -uri "https://graph.microsoft.com/v1.0/applications/$AppObjectId" -body $PatchBody -type PATCH -NoAuthCheck $true -AsApp $true + } else { + # Create new app registration + $State = 'created' + Write-Information "[SSO-App] Creating new app registration: $AppDisplayName" + + $CreateBody = @{ + displayName = $AppDisplayName + signInAudience = $SignInAudience + web = @{ + redirectUris = @($CallbackUri) + implicitGrantSettings = @{ enableIdTokenIssuance = $true } + } + requiredResourceAccess = @( + @{ + resourceAppId = $GraphResourceId + resourceAccess = $Permissions + } + ) + } | ConvertTo-Json -Depth 10 -Compress + + $NewApp = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/v1.0/applications' -body $CreateBody -type POST -NoAuthCheck $true -AsApp $true + $AppObjectId = $NewApp.id + $AppClientId = $NewApp.appId + Write-Information "[SSO-App] Created app: $AppClientId (objectId: $AppObjectId)" + + # Create service principal (idempotent — catch conflict) + $Attempt = 0 + $SpnCreated = $false + while ($Attempt -lt 3 -and -not $SpnCreated) { + try { + Start-Sleep -Seconds 2 + $SpnBody = @{ appId = $AppClientId } | ConvertTo-Json -Compress + New-GraphPOSTRequest -uri 'https://graph.microsoft.com/v1.0/servicePrincipals' -body $SpnBody -type POST -NoAuthCheck $true -AsApp $true | Out-Null + $SpnCreated = $true + Write-Information "[SSO-App] Service principal created for $AppClientId" + } catch { + $Attempt++ + Write-Information "[SSO-App] SPN creation attempt $Attempt failed (may already exist): $($_.Exception.Message)" + } + } + } + + # Handle app management policy exemption (same pattern as SAM setup) + try { + $PolicyStatus = Update-AppManagementPolicy -ApplicationId $AppClientId + Write-Information "[SSO-App] Policy exemption: $($PolicyStatus.PolicyAction)" + } catch { + 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 + } +} diff --git a/Modules/CIPPCore/Public/Authentication/Remove-CIPPMigrationAppSetting.ps1 b/Modules/CIPPCore/Public/Authentication/Remove-CIPPMigrationAppSetting.ps1 new file mode 100644 index 000000000000..118012870caf --- /dev/null +++ b/Modules/CIPPCore/Public/Authentication/Remove-CIPPMigrationAppSetting.ps1 @@ -0,0 +1,63 @@ +function Remove-CIPPMigrationAppSetting { + <# + .SYNOPSIS + Removes an app setting from the current App Service via ARM. + .DESCRIPTION + Reads the current app settings from ARM, removes the specified key, + and writes the updated settings back. Uses the managed identity for + authentication. Silently returns $false when not running in App Service. + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$SettingName + ) + + $SiteName = $env:WEBSITE_SITE_NAME + $ResourceGroup = $env:WEBSITE_RESOURCE_GROUP + $SubscriptionId = if ($env:WEBSITE_OWNER_NAME) { ($env:WEBSITE_OWNER_NAME -split '\+')[0] } else { $null } + + if (-not $SiteName -or -not $ResourceGroup -or -not $SubscriptionId) { + Write-Information "[Migration] Not running in App Service — cannot remove app setting '$SettingName'" + return $false + } + + if (-not $env:IDENTITY_ENDPOINT -or -not $env:IDENTITY_HEADER) { + Write-Information "[Migration] No managed identity available — cannot remove app setting '$SettingName'" + return $false + } + + # Get managed identity token for ARM + $TokenUri = "$($env:IDENTITY_ENDPOINT)?resource=https://management.azure.com/&api-version=2019-08-01" + $TokenResponse = Invoke-RestMethod -Uri $TokenUri -Headers @{ 'X-IDENTITY-HEADER' = $env:IDENTITY_HEADER } -Method Get + $ArmToken = $TokenResponse.access_token + + $BaseUri = "https://management.azure.com/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroup/providers/Microsoft.Web/sites/$SiteName" + $ArmHeaders = @{ + Authorization = "Bearer $ArmToken" + 'Content-Type' = 'application/json' + } + + # Read current app settings + $CurrentSettings = Invoke-RestMethod -Uri "$BaseUri/config/appsettings/list?api-version=2024-11-01" -Method Post -Headers @{ Authorization = "Bearer $ArmToken" } + $MergedSettings = @{} + if ($CurrentSettings.properties) { + $CurrentSettings.properties.PSObject.Properties | ForEach-Object { $MergedSettings[$_.Name] = $_.Value } + } + + if (-not $MergedSettings.ContainsKey($SettingName)) { + Write-Information "[Migration] App setting '$SettingName' not found — nothing to remove" + return $true + } + + $MergedSettings.Remove($SettingName) + + $SettingsBody = @{ properties = $MergedSettings } | ConvertTo-Json -Depth 5 + Invoke-RestMethod -Uri "$BaseUri/config/appsettings?api-version=2024-11-01" -Method Put -Headers $ArmHeaders -Body $SettingsBody + + Write-Information "[Migration] Removed app setting '$SettingName'" + Write-LogMessage -API 'SSO-Migration' -message "Removed app setting '$SettingName' after successful SSO migration" -sev Info + return $true +} diff --git a/Modules/CIPPCore/Public/Authentication/Set-CIPPSSOEasyAuth.ps1 b/Modules/CIPPCore/Public/Authentication/Set-CIPPSSOEasyAuth.ps1 new file mode 100644 index 000000000000..5ca8abadae37 --- /dev/null +++ b/Modules/CIPPCore/Public/Authentication/Set-CIPPSSOEasyAuth.ps1 @@ -0,0 +1,182 @@ +function Set-CIPPSSOEasyAuth { + <# + .SYNOPSIS + Configures or updates EasyAuth (authsettingsV2) on the current App Service. + .DESCRIPTION + Handles both initial EasyAuth setup and ongoing updates. For initial setup, + creates the full authsettingsV2 config with KV references for the client secret. + For updates, reads the existing config and patches the issuer URL. + Also manages the AUTH_SECRET app setting (using KV references) and + WEBSITE_AUTH_AAD_ALLOWED_TENANTS. + Only works inside an Azure App Service with a managed identity. + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$AppId, + + [Parameter(Mandatory)] + [bool]$MultiTenant, + + [Parameter(Mandatory)] + [string]$TenantId, + + [Parameter()] + [switch]$UseKvReferences, + + [Parameter()] + [switch]$ImplicitAuth + ) + + $SiteName = $env:WEBSITE_SITE_NAME + $ResourceGroup = $env:WEBSITE_RESOURCE_GROUP + $SubscriptionId = if ($env:WEBSITE_OWNER_NAME) { ($env:WEBSITE_OWNER_NAME -split '\+')[0] } else { $null } + + if (-not $SiteName -or -not $ResourceGroup -or -not $SubscriptionId) { + Write-Information '[SSO-EasyAuth] Not running in App Service — skipping EasyAuth config' + return $false + } + + if (-not $env:IDENTITY_ENDPOINT -or -not $env:IDENTITY_HEADER) { + Write-Information '[SSO-EasyAuth] No managed identity available — skipping EasyAuth config' + return $false + } + + # Get managed identity token for ARM + $TokenUri = "$($env:IDENTITY_ENDPOINT)?resource=https://management.azure.com/&api-version=2019-08-01" + $TokenResponse = Invoke-RestMethod -Uri $TokenUri -Headers @{ 'X-IDENTITY-HEADER' = $env:IDENTITY_HEADER } -Method Get + $ArmToken = $TokenResponse.access_token + + $BaseUri = "https://management.azure.com/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroup/providers/Microsoft.Web/sites/$SiteName" + $ArmHeaders = @{ + Authorization = "Bearer $ArmToken" + 'Content-Type' = 'application/json' + } + + $IssuerUrl = if ($MultiTenant) { + 'https://login.microsoftonline.com/common/v2.0' + } else { + "https://login.microsoftonline.com/$TenantId/v2.0" + } + + # Read current app settings and merge AUTH_SECRET + $CurrentSettings = Invoke-RestMethod -Uri "$BaseUri/config/appsettings/list?api-version=2024-11-01" -Method Post -Headers @{ Authorization = "Bearer $ArmToken" } + $MergedSettings = @{} + if ($CurrentSettings.properties) { + $CurrentSettings.properties.PSObject.Properties | ForEach-Object { $MergedSettings[$_.Name] = $_.Value } + } + + # Set AUTH_SECRET as a KV reference when requested (initial setup) + # Skip for implicit auth (no client secret needed — e.g. central migration app) + if ($UseKvReferences -and -not $ImplicitAuth) { + $KV = $env:WEBSITE_DEPLOYMENT_ID + $VaultName = if ($KV) { ($KV -split '-')[0] } else { $null } + if ($VaultName) { + $MergedSettings['AUTH_SECRET'] = "@Microsoft.KeyVault(VaultName=$VaultName;SecretName=SSOAppSecret)" + } + } + + # Always remove WEBSITE_AUTH_AAD_ALLOWED_TENANTS — we rely on the issuer URL + # for tenant restriction ("Use default restrictions based on issuer" in the portal). + # Multi-tenant uses common/v2.0 issuer, single-tenant uses {tenantId}/v2.0. + $MergedSettings.Remove('WEBSITE_AUTH_AAD_ALLOWED_TENANTS') + + $SettingsBody = @{ properties = $MergedSettings } | ConvertTo-Json -Depth 5 + Invoke-RestMethod -Uri "$BaseUri/config/appsettings?api-version=2024-11-01" -Method Put -Headers $ArmHeaders -Body $SettingsBody + + # Determine if we can read-modify-write (update) or need a full overwrite (initial setup) + if (-not $UseKvReferences -and $env:WEBSITE_AUTH_V2_CONFIG_JSON) { + # Read-modify-write: only patch the issuer URL, preserving existing allowedAudiences, + # allowedApplications, excludedPaths, tokenStore, etc. + $Current = $env:WEBSITE_AUTH_V2_CONFIG_JSON | ConvertFrom-Json -AsHashtable -Depth 20 + $ArmPayload = @{ properties = $Current } + + # 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 = @{} } + $AAD = $Current.identityProviders.azureActiveDirectory + + if (-not $AAD.ContainsKey('registration') -or $null -eq $AAD.registration) { $AAD.registration = @{} } + $AAD.registration.openIdIssuer = $IssuerUrl + + # Ensure the SSO app's own clientId is always in allowedAudiences and allowedApplications + if (-not $AAD.ContainsKey('validation') -or $null -eq $AAD.validation) { $AAD.validation = @{} } + + $ExistingAudiences = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) + if ($AAD.validation.allowedAudiences) { + foreach ($a in $AAD.validation.allowedAudiences) { [void]$ExistingAudiences.Add($a) } + } + [void]$ExistingAudiences.Add("api://$AppId") + $AAD.validation.allowedAudiences = @($ExistingAudiences) + + if (-not $AAD.validation.ContainsKey('defaultAuthorizationPolicy') -or $null -eq $AAD.validation.defaultAuthorizationPolicy) { + $AAD.validation.defaultAuthorizationPolicy = @{} + } + $ExistingApps = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) + if ($AAD.validation.defaultAuthorizationPolicy.allowedApplications) { + foreach ($a in $AAD.validation.defaultAuthorizationPolicy.allowedApplications) { [void]$ExistingApps.Add($a) } + } + [void]$ExistingApps.Add($AppId) + $AAD.validation.defaultAuthorizationPolicy.allowedApplications = @($ExistingApps) + + if (-not $AAD.validation.defaultAuthorizationPolicy.ContainsKey('allowedPrincipals')) { + $AAD.validation.defaultAuthorizationPolicy.allowedPrincipals = @{} + } + + $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 { + # Full overwrite: initial setup — build the entire authsettingsV2 from scratch + $AuthConfig = @{ + properties = @{ + platform = @{ enabled = $true } + globalValidation = @{ + unauthenticatedClientAction = 'RedirectToLoginPage' + redirectToProvider = 'azureactivedirectory' + excludedPaths = @( + '/api/Public*' + '/api/setup/health' + ) + } + identityProviders = @{ + azureActiveDirectory = @{ + enabled = $true + registration = $(if ($ImplicitAuth) { + @{ + clientId = $AppId + openIdIssuer = $IssuerUrl + } + } else { + @{ + clientId = $AppId + clientSecretSettingName = 'AUTH_SECRET' + openIdIssuer = $IssuerUrl + } + }) + validation = @{ + allowedAudiences = @("api://$AppId") + defaultAuthorizationPolicy = @{ + allowedPrincipals = @{} + allowedApplications = @($AppId) + } + } + } + } + login = @{ + tokenStore = @{ + enabled = $true + tokenRefreshExtensionHours = 72 + } + } + } + } | ConvertTo-Json -Depth 20 + } + + Invoke-RestMethod -Uri "$BaseUri/config/authsettingsV2?api-version=2020-06-01" -Method Put -Headers $ArmHeaders -Body $AuthConfig + + Write-Information "[SSO-EasyAuth] Configured EasyAuth: appId=$AppId, issuer=$IssuerUrl, multiTenant=$MultiTenant" + Write-LogMessage -API 'SSO-EasyAuth' -message "EasyAuth configured: appId=$AppId, issuer=$IssuerUrl" -sev Info + return $true +} diff --git a/Modules/CIPPCore/Public/Authentication/Set-CippApiAuth.ps1 b/Modules/CIPPCore/Public/Authentication/Set-CippApiAuth.ps1 index 91e40acb6290..1e1dc84a1fb6 100644 --- a/Modules/CIPPCore/Public/Authentication/Set-CippApiAuth.ps1 +++ b/Modules/CIPPCore/Public/Authentication/Set-CippApiAuth.ps1 @@ -7,67 +7,144 @@ function Set-CippApiAuth { [string[]]$ClientIds ) - # Resolve subscription ID via helper (managed identity environment assumed for ARM). - $SubscriptionId = Get-CIPPAzFunctionAppSubId + if ($env:CIPPNG) { + # Read-modify-write — only patch allowedApplications + allowedAudiences, + # preserving the SSO EasyAuth config (clientSecretSettingName, excludedPaths, tokenStore, etc.) - # 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 } + # Resolve env vars directly (same pattern as Set-CIPPSSOEasyAuth which works) + $SiteName = $env:WEBSITE_SITE_NAME + $ResourceGroup = $env:WEBSITE_RESOURCE_GROUP + $SubscriptionId = if ($env:WEBSITE_OWNER_NAME) { ($env:WEBSITE_OWNER_NAME -split '\+')[0] } else { $null } - Write-Information "AuthSettings: $($AuthSettings | ConvertTo-Json -Depth 10)" + Write-Information "[ApiAuth] SiteName=$SiteName, ResourceGroup=$ResourceGroup, SubscriptionId=$SubscriptionId" + Write-Information "[ApiAuth] ClientIds to set: $($ClientIds -join ', ')" - # Set allowed audiences - $AllowedAudiences = foreach ($ClientId in $ClientIds) { - "api://$ClientId" - } + 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" + } - if (!$AllowedAudiences) { $AllowedAudiences = @() } - if (!$ClientIds) { $ClientIds = @() } + $BaseUri = "https://management.azure.com/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroup/providers/Microsoft.Web/sites/$SiteName" + Write-Information "[ApiAuth] BaseUri=$BaseUri" - # Set auth settings + # Read current authsettingsV2 from platform-injected env var (reliable, no ARM call needed) + # Then convert to deep hashtable for safe mutation + if (-not $env:WEBSITE_AUTH_V2_CONFIG_JSON) { + throw '[ApiAuth] WEBSITE_AUTH_V2_CONFIG_JSON env var not set — EasyAuth may not be configured yet.' + } - if (($ClientIds | Measure-Object).Count -gt 0) { - $AuthSettings.properties.identityProviders.azureActiveDirectory = @{ - enabled = $true - registration = @{ - clientId = $ClientIds[0] ?? $ClientIds - openIdIssuer = "https://sts.windows.net/$TenantID/v2.0" - } - validation = @{ - allowedAudiences = @($AllowedAudiences) - defaultAuthorizationPolicy = @{ - allowedApplications = @($ClientIds) - } - } + $Current = $env:WEBSITE_AUTH_V2_CONFIG_JSON | ConvertFrom-Json -AsHashtable -Depth 20 + # Wrap in properties envelope for the ARM PUT (env var has the raw config, ARM expects {properties: ...}) + $ArmPayload = @{ properties = $Current } + + Write-Information "[ApiAuth] Read config from env var OK. Keys=$($Current.Keys -join ', ')" + + # 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 = @{} } + + $AAD = $Current.identityProviders.azureActiveDirectory + Write-Information "[ApiAuth] AAD keys: $($AAD.Keys -join ', ')" + + # The SSO app's clientId is the registration clientId — always keep it in the lists + $SSOClientId = $AAD.registration.clientId + Write-Information "[ApiAuth] SSO clientId from registration: $SSOClientId" + + # Merge: SSO app + all enabled API clients + $AllAppIds = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) + if ($SSOClientId) { [void]$AllAppIds.Add($SSOClientId) } + foreach ($id in $ClientIds) { + if (-not [string]::IsNullOrEmpty($id)) { [void]$AllAppIds.Add($id) } + } + + # Build allowed audiences: api://{id} for each + $AllAudiences = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) + foreach ($id in $AllAppIds) { + [void]$AllAudiences.Add("api://$id") + } + + Write-Information "[ApiAuth] Merged allowedApplications: $($AllAppIds -join ', ')" + Write-Information "[ApiAuth] Merged allowedAudiences: $($AllAudiences -join ', ')" + + # Ensure every level of the validation path exists + if (-not $AAD.ContainsKey('validation') -or $null -eq $AAD.validation) { + $AAD.validation = @{} + } + $AAD.validation.allowedAudiences = @($AllAudiences) + + if (-not $AAD.validation.ContainsKey('defaultAuthorizationPolicy') -or $null -eq $AAD.validation.defaultAuthorizationPolicy) { + $AAD.validation.defaultAuthorizationPolicy = @{} + } + $AAD.validation.defaultAuthorizationPolicy.allowedApplications = @($AllAppIds) + + # Ensure allowedPrincipals exists (for "use default restrictions based on issuer") + if (-not $AAD.validation.defaultAuthorizationPolicy.ContainsKey('allowedPrincipals')) { + $AAD.validation.defaultAuthorizationPolicy.allowedPrincipals = @{} + } + + $PutBody = $ArmPayload | ConvertTo-Json -Depth 20 + Write-Information "[ApiAuth] PUT body: $PutBody" + + if ($PSCmdlet.ShouldProcess('Update authsettingsV2 (read-modify-write)')) { + $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" } } else { - $AuthSettings.properties.identityProviders.azureActiveDirectory = @{ - enabled = $false - registration = @{} - validation = @{} + # Full overwrite path (no SSO EasyAuth config to preserve) + $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 + + Write-Information "AuthSettings: $($AuthSettings | ConvertTo-Json -Depth 10)" + + $AllowedAudiences = foreach ($ClientId in $ClientIds) { "api://$ClientId" } + if (!$AllowedAudiences) { $AllowedAudiences = @() } + if (!$ClientIds) { $ClientIds = @() } + + if (($ClientIds | Measure-Object).Count -gt 0) { + $AuthSettings.properties.identityProviders.azureActiveDirectory = @{ + enabled = $true + registration = @{ + clientId = $ClientIds[0] ?? $ClientIds + openIdIssuer = "https://sts.windows.net/$TenantID/v2.0" + } + validation = @{ + allowedAudiences = @($AllowedAudiences) + defaultAuthorizationPolicy = @{ + allowedApplications = @($ClientIds) + } + } + } + } else { + $AuthSettings.properties.identityProviders.azureActiveDirectory = @{ + enabled = $false + registration = @{} + validation = @{} + } } - } - $AuthSettings.properties.globalValidation = @{ - unauthenticatedClientAction = 'Return401' - } - $AuthSettings.properties.login = @{ - tokenStore = @{ - enabled = $true - tokenRefreshExtensionHours = 72 + $AuthSettings.properties.globalValidation = @{ + unauthenticatedClientAction = 'Return401' + } + $AuthSettings.properties.login = @{ + tokenStore = @{ + enabled = $true + tokenRefreshExtensionHours = 72 + } } - } - if ($PSCmdlet.ShouldProcess('Update auth settings')) { - # 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 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' + } - if ($PSCmdlet.ShouldProcess('Update allowed tenants')) { - $null = Update-CIPPAzFunctionAppSetting -Name $FunctionAppName -ResourceGroupName $RGName -AppSetting @{ 'WEBSITE_AUTH_AAD_ALLOWED_TENANTS' = $TenantId } + if ($PSCmdlet.ShouldProcess('Update allowed tenants')) { + $null = Update-CIPPAzFunctionAppSetting -Name $FunctionAppName -ResourceGroupName $RGName -AppSetting @{ 'WEBSITE_AUTH_AAD_ALLOWED_TENANTS' = $TenantId } + } } } diff --git a/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 b/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 index 2ad765e41bdf..9622bf5f0c9e 100644 --- a/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 +++ b/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 @@ -202,13 +202,42 @@ function Test-CIPPAccess { $Permissions = Get-CippAllowedPermissions -UserRoles $User.userRoles $swPermsMe.Stop() $AccessTimings['GetPermissions(me)'] = $swPermsMe.Elapsed.TotalMilliseconds + + # Include SSO migration status for admins with AppSettings permissions + $MeResponse = @{ + 'clientPrincipal' = $User + 'permissions' = $Permissions + } + + # 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'] = @{ + appId = $env:CIPP_SSO_MIGRATION_APPID + status = 'pending' + } + } + + if ($Permissions -contains 'CIPP.AppSettings.ReadWrite' -and $env:CIPPNG -ne 'true' -and $env:CIPP_SSO_MIGRATION_PROMPT -eq 'true') { + try { + $SSOTable = Get-CIPPTable -tablename 'SSOMigration' + $SSOMigration = Get-CIPPAzDataTableEntity @SSOTable -Filter "PartitionKey eq 'SSO' and RowKey eq 'MigrationConfig'" -ErrorAction SilentlyContinue + if ($SSOMigration) { + $MeResponse['ssoMigration'] = @{ + status = $SSOMigration.Status + appId = $SSOMigration.AppId + multiTenant = [bool]($SSOMigration.MultiTenant -eq 'true' -or $SSOMigration.MultiTenant -eq 'True') + } + } else { + $MeResponse['ssoMigration'] = @{ status = 'none' } + } + } catch { + $MeResponse['ssoMigration'] = @{ status = 'none' } + } + } + return ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::OK - Body = ( - @{ - 'clientPrincipal' = $User - 'permissions' = $Permissions - } | ConvertTo-Json -Depth 5) + Body = ($MeResponse | ConvertTo-Json -Depth 5) }) } diff --git a/Modules/CIPPCore/Public/Authentication/Update-CIPPSSORedirectUri.ps1 b/Modules/CIPPCore/Public/Authentication/Update-CIPPSSORedirectUri.ps1 new file mode 100644 index 000000000000..03d113d2a214 --- /dev/null +++ b/Modules/CIPPCore/Public/Authentication/Update-CIPPSSORedirectUri.ps1 @@ -0,0 +1,126 @@ +function Update-CIPPSSORedirectUri { + <# + .SYNOPSIS + Ensures the CIPP-SSO app registration includes redirect URIs for all bound hostnames + and that signInAudience matches the stored multi-tenant flag. + + .DESCRIPTION + Reads the stored SSO AppId and MultiTenant flag from Key Vault (or DevSecrets table + in dev mode), then: + 1. Queries ARM for all hostnames bound to the App Service (custom domains + default). + 2. Ensures the SSO app's web.redirectUris includes a callback URI for each hostname. + 3. Verifies and patches signInAudience on the app reg if it doesn't match the stored + multi-tenant flag (AzureADMyOrg for single-tenant, AzureADMultipleOrgs for multi). + #> + [CmdletBinding()] + param() + + $CurrentHost = $env:WEBSITE_HOSTNAME + if (-not $CurrentHost) { + Write-Information '[SSO-Redirect] WEBSITE_HOSTNAME not set, skipping redirect URI update' + return + } + + # Resolve the stored SSO AppId and MultiTenant flag + $SSOAppId = $null + $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 + $SSOAppId = $Secret.SSOAppId + $SSOMultiTenant = $Secret.SSOMultiTenant -eq 'True' + } catch { } + } else { + $KV = $env:WEBSITE_DEPLOYMENT_ID + $VaultName = if ($KV) { ($KV -split '-')[0] } else { $null } + if ($VaultName) { + try { + $SSOAppId = Get-CippKeyVaultSecret -VaultName $VaultName -Name 'SSOAppId' -AsPlainText -ErrorAction Stop + } catch { } + try { + $mtVal = Get-CippKeyVaultSecret -VaultName $VaultName -Name 'SSOMultiTenant' -AsPlainText -ErrorAction Stop + $SSOMultiTenant = $mtVal -eq 'True' + } catch { } + } + } + + if (-not $SSOAppId) { + Write-Information '[SSO-Redirect] No SSO AppId found, skipping redirect URI update' + return + } + + # Discover all bound hostnames via ARM (custom domains + default) + $AllHostnames = @($CurrentHost) + try { + $SiteName = $env:WEBSITE_SITE_NAME + $ResourceGroup = $env:WEBSITE_RESOURCE_GROUP + $SubscriptionId = if ($env:WEBSITE_OWNER_NAME) { ($env:WEBSITE_OWNER_NAME -split '\+')[0] } else { $null } + + if ($SiteName -and $ResourceGroup -and $SubscriptionId -and $env:IDENTITY_ENDPOINT -and $env:IDENTITY_HEADER) { + $TokenUri = "$($env:IDENTITY_ENDPOINT)?resource=https://management.azure.com/&api-version=2019-08-01" + $TokenResponse = Invoke-RestMethod -Uri $TokenUri -Headers @{ 'X-IDENTITY-HEADER' = $env:IDENTITY_HEADER } -Method Get + $ArmToken = $TokenResponse.access_token + + $SiteUri = "https://management.azure.com/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroup/providers/Microsoft.Web/sites/$SiteName`?api-version=2024-11-01" + $SiteResponse = Invoke-RestMethod -Uri $SiteUri -Headers @{ Authorization = "Bearer $ArmToken" } -Method Get + + if ($SiteResponse.properties.hostNames) { + $AllHostnames = @($SiteResponse.properties.hostNames) + Write-Information "[SSO-Redirect] Discovered hostnames from ARM: $($AllHostnames -join ', ')" + } + } + } catch { + Write-Information "[SSO-Redirect] ARM hostname discovery failed (using WEBSITE_HOSTNAME only): $($_.Exception.Message)" + } + + # Build required redirect URIs from all hostnames + $RequiredUris = foreach ($Hostname in $AllHostnames) { + "https://$Hostname/.auth/login/aad/callback" + } + + try { + $AppResponse = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/applications(appId='$SSOAppId')?`$select=id,web,signInAudience" -NoAuthCheck $true -AsApp $true + $ExistingUris = @($AppResponse.web.redirectUris) + + # Determine which URIs are missing + $MissingUris = $RequiredUris | Where-Object { $_ -notin $ExistingUris } + + # Determine the expected signInAudience + $ExpectedAudience = if ($SSOMultiTenant) { 'AzureADMultipleOrgs' } else { 'AzureADMyOrg' } + $AudienceMismatch = $AppResponse.signInAudience -ne $ExpectedAudience + + if ($MissingUris.Count -eq 0 -and -not $AudienceMismatch) { + Write-Information '[SSO-Redirect] All redirect URIs present and signInAudience correct' + return + } + + # Build patch body + $PatchBody = @{} + + if ($MissingUris.Count -gt 0) { + $UpdatedUris = [System.Collections.Generic.List[string]]::new() + $ExistingUris | ForEach-Object { $UpdatedUris.Add($_) } + $MissingUris | ForEach-Object { $UpdatedUris.Add($_) } + $PatchBody.web = @{ redirectUris = $UpdatedUris } + } + + if ($AudienceMismatch) { + $PatchBody.signInAudience = $ExpectedAudience + Write-Information "[SSO-Redirect] Correcting signInAudience: $($AppResponse.signInAudience) -> $ExpectedAudience" + } + + $Body = $PatchBody | ConvertTo-Json -Depth 5 + New-GraphPOSTRequest -uri "https://graph.microsoft.com/v1.0/applications/$($AppResponse.id)" -body $Body -type PATCH -NoAuthCheck $true -AsApp $true + + if ($MissingUris.Count -gt 0) { + Write-Information "[SSO-Redirect] Added redirect URIs: $($MissingUris -join ', ')" + Write-LogMessage -API 'SSO-Redirect' -message "Added redirect URIs: $($MissingUris -join ', ')" -sev Info + } + if ($AudienceMismatch) { + Write-LogMessage -API 'SSO-Redirect' -message "Updated signInAudience to $ExpectedAudience (multiTenant=$SSOMultiTenant)" -sev Info + } + } catch { + Write-LogMessage -API 'SSO-Redirect' -message "Failed to update SSO app registration: $_" -LogData (Get-CippException -Exception $_) -sev Warning + } +} 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/CIPPCore/Public/Entrypoints/Timer Functions/Start-ContainerUpdateCheck.ps1 b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-ContainerUpdateCheck.ps1 new file mode 100644 index 000000000000..381abbbc023c --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-ContainerUpdateCheck.ps1 @@ -0,0 +1,193 @@ +function Start-ContainerUpdateCheck { + <# + .SYNOPSIS + Timer function to check for container image updates + .DESCRIPTION + Reads update settings from ContainerUpdateSettings table, checks if it's time to run based + on the configured interval and check time, queries GHCR for the latest image digest, and + optionally triggers a restart if auto-update is enabled. + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param() + + if ($PSCmdlet.ShouldProcess('Start-ContainerUpdateCheck', 'Check for container image updates')) { + $SettingsTable = Get-CippTable -tablename 'ContainerUpdateSettings' + $Settings = Get-CIPPAzDataTableEntity @SettingsTable -Filter "PartitionKey eq 'Settings' and RowKey eq 'UpdateConfig'" | Select-Object -First 1 + + if (-not $Settings -or $Settings.CheckInterval -eq '0' -or [string]::IsNullOrWhiteSpace($Settings.CheckInterval)) { + Write-Information 'Container update check: disabled or not configured' + return + } + + # Parse interval to determine if we're due + $IntervalHours = switch ($Settings.CheckInterval) { + '1h' { 1 } + '4h' { 4 } + '12h' { 12 } + '1d' { 24 } + default { 0 } + } + if ($IntervalHours -eq 0) { + Write-Information "Container update check: unknown interval '$($Settings.CheckInterval)'" + return + } + + # Check if preferred time applies — within 45 minutes of desired hour using CIPP timezone + $CheckTime = $Settings.CheckTime + if ($CheckTime -and [string]$CheckTime -ne '') { + $TargetHour = [int]$CheckTime + + # Load the configured CIPP timezone (same source as Get-CIPPTimerFunctions) + $ConfigTable = Get-CIPPTable -tablename Config + $TimeSettings = Get-CIPPAzDataTableEntity @ConfigTable -Filter "PartitionKey eq 'TimeSettings' and RowKey eq 'TimeSettings'" | Select-Object -First 1 + $ScheduleTimeZone = [TimeZoneInfo]::Utc + if ($TimeSettings.Timezone) { + try { + $ScheduleTimeZone = [TimeZoneInfo]::FindSystemTimeZoneById($TimeSettings.Timezone) + } catch { + Write-Information "Invalid timezone '$($TimeSettings.Timezone)' — falling back to UTC" + } + } + + # Convert current UTC time to the configured timezone + $NowUtc = [DateTime]::UtcNow + $NowLocal = [TimeZoneInfo]::ConvertTimeFromUtc($NowUtc, $ScheduleTimeZone) + $Today = $NowLocal.Date + $TargetTime = $Today.AddHours($TargetHour) + $MinutesDiff = [math]::Abs(($NowLocal - $TargetTime).TotalMinutes) + # Handle wrapping around midnight (e.g. target=23, current=0) + $MinutesDiffWrap = 1440 - $MinutesDiff + $EffectiveDiff = [math]::Min($MinutesDiff, $MinutesDiffWrap) + if ($EffectiveDiff -gt 45) { + Write-Information "Container update check: not within 45 min of preferred time ($($TargetHour):00 $($ScheduleTimeZone.Id)), current local: $($NowLocal.ToString('HH:mm')), diff: $([math]::Round($EffectiveDiff))min" + return + } + } + + # Check if enough time has elapsed since last check + $LastCheck = $Settings.LastCheck + if ($LastCheck) { + try { + $LastCheckEpoch = [int64]$LastCheck + $LastCheckTime = [DateTimeOffset]::FromUnixTimeSeconds($LastCheckEpoch).UtcDateTime + $ElapsedHours = ((Get-Date).ToUniversalTime() - $LastCheckTime).TotalHours + if ($ElapsedHours -lt ($IntervalHours * 0.9)) { + Write-Information "Container update check: last check was $([math]::Round($ElapsedHours, 1))h ago, interval is ${IntervalHours}h — skipping" + return + } + } catch { + Write-Information "Container update check: could not parse LastCheck '$LastCheck' — proceeding" + } + } + + Write-Information 'Container update check: running' + + try { + # Resolve ARM site details + $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 + } + } + + $ImageTag = $env:IMAGE_TAG ?? 'unknown' + $CurrentImage = $null + + if ($Subscription -and $RGName -and $SiteName) { + $apiVersion = '2024-11-01' + $uri = "https://management.azure.com/subscriptions/$Subscription/resourceGroups/$RGName/providers/Microsoft.Web/sites/$SiteName/config/web?api-version=$apiVersion" + $webConfig = New-CIPPAzRestRequest -Uri $uri -Method GET + $linuxFxVersion = $webConfig.properties.linuxFxVersion + if ($linuxFxVersion) { + $CurrentImage = $linuxFxVersion -replace '^DOCKER\|', '' + } + } + + if (-not $CurrentImage) { + Write-LogMessage -API 'ContainerUpdateCheck' -message 'Could not determine current container image from ARM' -sev Warning + return + } + + # Update checking only works with GHCR-hosted images + if ($CurrentImage -notmatch '^ghcr\.io/') { + Write-Information "Container update check: skipped — image '$CurrentImage' is not hosted on GHCR" + return + } + + $CheckTag = if ($CurrentImage -match ':([^:]+)$') { $Matches[1] } else { $ImageTag } + + # Parse image path for GHCR + $imagePath = $CurrentImage -replace '^ghcr\.io/', '' -replace ':.*$', '' + if (-not $imagePath) { + Write-LogMessage -API 'ContainerUpdateCheck' -message 'Could not parse image path from reference' -sev Warning + return + } + + # Get anonymous GHCR token + $tokenUri = "https://ghcr.io/token?scope=repository:${imagePath}:pull" + $tokenResp = Invoke-RestMethod -Uri $tokenUri -Method GET -ErrorAction Stop + $token = $tokenResp.token + + $digestHeaders = @{ + Authorization = "Bearer $token" + Accept = 'application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.docker.distribution.manifest.v2+json' + } + + # Get remote digest for the configured channel tag + $manifestUri = "https://ghcr.io/v2/$imagePath/manifests/$CheckTag" + $resp = Invoke-WebRequest -Uri $manifestUri -Method HEAD -Headers $digestHeaders -ErrorAction Stop + $RemoteDigest = $resp.Headers['Docker-Content-Digest'] + if ($RemoteDigest -is [array]) { $RemoteDigest = $RemoteDigest[0] } + + # Get running digest for the baked-in image tag + $RunningDigest = $null + try { + $runningUri = "https://ghcr.io/v2/$imagePath/manifests/$ImageTag" + $runResp = Invoke-WebRequest -Uri $runningUri -Method HEAD -Headers $digestHeaders -ErrorAction Stop + $RunningDigest = $runResp.Headers['Docker-Content-Digest'] + if ($RunningDigest -is [array]) { $RunningDigest = $RunningDigest[0] } + } catch { + Write-Information "Could not get running digest for tag $ImageTag" + } + + $UpdateAvailable = $false + if ($RemoteDigest -and $RunningDigest -and $RemoteDigest -ne $RunningDigest) { + $UpdateAvailable = $true + } + + # Update the settings row with results (preserve user settings) + $UpdateEntity = @{ + PartitionKey = 'Settings' + RowKey = 'UpdateConfig' + AutoUpdate = [string]($Settings.AutoUpdate ?? 'false') + CheckInterval = [string]($Settings.CheckInterval ?? '0') + CheckTime = [string]($Settings.CheckTime ?? '') + LastCheck = [string][int64](([DateTimeOffset]::UtcNow).ToUnixTimeSeconds()) + UpdateAvailable = [string]$UpdateAvailable + RunningDigest = [string]($RunningDigest ?? '') + RemoteDigest = [string]($RemoteDigest ?? '') + } + Add-CIPPAzDataTableEntity @SettingsTable -Entity $UpdateEntity -Force | Out-Null + + 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') + } catch { + Write-LogMessage -API 'ContainerUpdateCheck' -message 'Auto-restart requested but AppLifecycleBridge is not available' -sev Warning + } + } elseif ($UpdateAvailable) { + Write-LogMessage -API 'ContainerUpdateCheck' -message "Container update available (running: $RunningDigest, remote: $RemoteDigest)" -sev Info + } else { + Write-Information "Container is up to date. Digest: $RunningDigest" + } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'ContainerUpdateCheck' -message "Failed to check for container update: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + } + } +} diff --git a/Modules/CIPPCore/Public/Functions/Test-CIPPStandardLicense.ps1 b/Modules/CIPPCore/Public/Functions/Test-CIPPStandardLicense.ps1 index 851822afe56c..4a998a6e6a9f 100644 --- a/Modules/CIPPCore/Public/Functions/Test-CIPPStandardLicense.ps1 +++ b/Modules/CIPPCore/Public/Functions/Test-CIPPStandardLicense.ps1 @@ -10,13 +10,17 @@ function Test-CIPPStandardLicense { .PARAMETER TenantFilter The tenant to check licensing for .PARAMETER RequiredCapabilities - Array of required capabilities for the standard + Array of required capabilities for the standard. Can be combined with Preset for edge cases. + .PARAMETER Preset + One or more predefined capability sets to check for the standard .FUNCTIONALITY Internal .EXAMPLE Test-CIPPStandardLicense -StandardName "ConditionalAccessTemplate" -TenantFilter "contoso.onmicrosoft.com" -RequiredCapabilities @('AADPremiumService') .EXAMPLE Test-CIPPStandardLicense -StandardName "SafeLinksPolicy" -TenantFilter "contoso.onmicrosoft.com" -RequiredCapabilities @('DEFENDER_FOR_OFFICE_365_PLAN_1', 'DEFENDER_FOR_OFFICE_365_PLAN_2') + .EXAMPLE + Test-CIPPStandardLicense -StandardName "TeamsGuestAccess" -TenantFilter "contoso.onmicrosoft.com" -Preset Teams #> [CmdletBinding()] param( @@ -26,13 +30,48 @@ function Test-CIPPStandardLicense { [Parameter(Mandatory = $true)] [string]$TenantFilter, - [Parameter(Mandatory = $true)] + [Parameter(Mandatory = $false)] [string[]]$RequiredCapabilities, + [Parameter(Mandatory = $false)] + [ValidateSet('Exchange', 'SharePoint', 'Intune', 'Entra', 'EntraP2', 'Teams', 'Compliance', 'DefenderForOffice365')] + [string[]]$Preset, + [Parameter(Mandatory = $false)] [switch]$SkipLog ) + $Presets = @{ + Exchange = @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', + 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', + 'EXCHANGE_LITE') + SharePoint = @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', + 'SHAREPOINTENTERPRISE_EDU', 'SHAREPOINTENTERPRISE_GOV', + 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') + Intune = @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') + Entra = @('AAD_PREMIUM', 'AAD_PREMIUM_P2') + EntraP2 = @('AAD_PREMIUM_P2') + Teams = @('MCOSTANDARD', 'MCOEV', 'MCOIMP', 'TEAMS1', 'Teams_Room_Standard') + Compliance = @('RMS_S_PREMIUM', 'RMS_S_PREMIUM2', 'MIP_S_CLP1', 'MIP_S_CLP2') + DefenderForOffice365 = @( + 'ATP_ENTERPRISE', 'ATP_ENTERPRISE_GOV', + 'THREAT_INTELLIGENCE', 'THREAT_INTELLIGENCE_GOV' + ) + } + + if ((!$Preset -or $Preset.Count -eq 0) -and (!$RequiredCapabilities -or $RequiredCapabilities.Count -eq 0)) { + throw 'Test-CIPPStandardLicense requires either -Preset or -RequiredCapabilities.' + } + + if ($Preset) { + $RequiredCapabilities = @( + $RequiredCapabilities + foreach ($CapabilityPreset in $Preset) { + $Presets[$CapabilityPreset] + } + ) | Where-Object { $_ } | Select-Object -Unique + } + try { $TenantCapabilities = Get-CIPPTenantCapabilities -TenantFilter $TenantFilter diff --git a/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 b/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 index effd7c8dd5b3..a4e159bbfbce 100644 --- a/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 @@ -29,8 +29,8 @@ function Get-CIPPDrift { [switch]$AllTenants ) - $IntuneCapable = Test-CIPPStandardLicense -StandardName 'IntuneTemplate_general' -TenantFilter $TenantFilter -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') - $ConditionalAccessCapable = Test-CIPPStandardLicense -StandardName 'ConditionalAccessTemplate_general' -TenantFilter $TenantFilter -RequiredCapabilities @('AAD_PREMIUM', 'AAD_PREMIUM_P2') + $IntuneCapable = Test-CIPPStandardLicense -StandardName 'IntuneTemplate_general' -TenantFilter $TenantFilter -Preset Intune + $ConditionalAccessCapable = Test-CIPPStandardLicense -StandardName 'ConditionalAccessTemplate_general' -TenantFilter $TenantFilter -Preset Entra $IntuneTable = Get-CippTable -tablename 'templates' # Load only IntuneTemplate partition for tag resolution and display name lookup diff --git a/Modules/CIPPCore/Public/Get-CIPPFeatureFlag.ps1 b/Modules/CIPPCore/Public/Get-CIPPFeatureFlag.ps1 index f5d7963d2e59..779c3f13789a 100644 --- a/Modules/CIPPCore/Public/Get-CIPPFeatureFlag.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPFeatureFlag.ps1 @@ -45,6 +45,7 @@ function Get-CIPPFeatureFlag { Timers = $FeatureFlag.Timers Endpoints = $FeatureFlag.Endpoints Pages = $FeatureFlag.Pages + Hidden = [bool]$FeatureFlag.Hidden Enabled = $TableFlag.Enabled } } else { @@ -65,6 +66,7 @@ function Get-CIPPFeatureFlag { Timers = $FeatureFlag.Timers Endpoints = $FeatureFlag.Endpoints Pages = $FeatureFlag.Pages + Hidden = [bool]$FeatureFlag.Hidden Enabled = $FeatureFlag.Enabled } } @@ -83,6 +85,7 @@ function Get-CIPPFeatureFlag { Timers = $FeatureFlag.Timers Endpoints = $FeatureFlag.Endpoints Pages = $FeatureFlag.Pages + Hidden = [bool]$FeatureFlag.Hidden Enabled = $TableFlag.Enabled } } else { @@ -103,6 +106,7 @@ function Get-CIPPFeatureFlag { Timers = $FeatureFlag.Timers Endpoints = $FeatureFlag.Endpoints Pages = $FeatureFlag.Pages + Hidden = [bool]$FeatureFlag.Hidden Enabled = $FeatureFlag.Enabled } } diff --git a/Modules/CIPPCore/Public/Get-CIPPTextReplacement.ps1 b/Modules/CIPPCore/Public/Get-CIPPTextReplacement.ps1 index 3b2db7623b7e..f96bef07090c 100644 --- a/Modules/CIPPCore/Public/Get-CIPPTextReplacement.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPTextReplacement.ps1 @@ -65,11 +65,12 @@ function Get-CIPPTextReplacement { $Vars = @{} if ($GlobalMap) { foreach ($Var in $GlobalMap) { + if (-not $Var.PSObject.Properties['Value']) { continue } + $Val = $Var.Value if ($EscapeForJson.IsPresent) { - # Escape quotes for JSON if not already escaped - $Var.Value = $Var.Value -replace '(?" from existing policy try { $ExistingVacationGroup = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/groups?`$filter=startsWith(displayName,'Vacation Exclusion')&`$select=id,displayName&`$top=999&`$count=true" -ComplexFilter -tenantid $TenantFilter -asApp $true | - Where-Object { $CheckExisting.conditions.users.excludeGroups -contains $_.id } + Where-Object { $CheckExisting.conditions.users.excludeGroups -contains $_.id } if ($ExistingVacationGroup) { if (-not ($JSONobj.conditions.users.PSObject.Properties.Name -contains 'excludeGroups')) { $JSONobj.conditions.users | Add-Member -NotePropertyName 'excludeGroups' -NotePropertyValue @() -Force diff --git a/Modules/CIPPCore/Public/New-CIPPCATemplate.ps1 b/Modules/CIPPCore/Public/New-CIPPCATemplate.ps1 index 6103d5ad945c..677377cbaee0 100644 --- a/Modules/CIPPCore/Public/New-CIPPCATemplate.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPCATemplate.ps1 @@ -38,6 +38,12 @@ function New-CIPPCATemplate { } } + # Fetch authentication context class references if the policy uses them + $authContexts = $null + if ($JSON.conditions.applications.includeAuthenticationContextClassReferences) { + $authContexts = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/authenticationContextClassReferences' -tenantid $TenantFilter + } + $AllLocations = [system.collections.generic.list[object]]::new() $includelocations = [system.collections.generic.list[object]]::new() @@ -113,6 +119,24 @@ function New-CIPPCATemplate { # Remove duplicates based on displayName to avoid Select-Object -Unique issues with complex objects $UniqueLocations = $AllLocations | Group-Object -Property displayName | ForEach-Object { $_.Group[0] } $JSON | Add-Member -NotePropertyName 'LocationInfo' -NotePropertyValue @($UniqueLocations) -Force + + # Convert authentication context class reference IDs to display names and store full objects + if ($authContexts -and $JSON.conditions.applications.includeAuthenticationContextClassReferences) { + $AllAuthContexts = [System.Collections.Generic.List[object]]::new() + $authContextDisplayNames = [System.Collections.Generic.List[object]]::new() + foreach ($acr in $JSON.conditions.applications.includeAuthenticationContextClassReferences) { + $acrInfo = $authContexts | Where-Object -Property id -EQ $acr | Select-Object id, displayName, description, isAvailable + if ($acrInfo) { + $authContextDisplayNames.Add($acrInfo.displayName) + $AllAuthContexts.Add($acrInfo) + } else { + $authContextDisplayNames.Add($acr) + } + } + $JSON.conditions.applications.includeAuthenticationContextClassReferences = @($authContextDisplayNames) + $JSON | Add-Member -NotePropertyName 'AuthContextInfo' -NotePropertyValue @($AllAuthContexts) -Force + } + $JSON = (ConvertTo-Json -Compress -Depth 100 -InputObject $JSON) return $JSON } diff --git a/Modules/CIPPCore/Public/Search-CIPPDbData.ps1 b/Modules/CIPPCore/Public/Search-CIPPDbData.ps1 index cf09305a8dd6..ab03c35f5771 100644 --- a/Modules/CIPPCore/Public/Search-CIPPDbData.ps1 +++ b/Modules/CIPPCore/Public/Search-CIPPDbData.ps1 @@ -69,7 +69,7 @@ function Search-CIPPDbData { 'Groups', 'Roles', 'LicenseOverview', 'IntuneDeviceCompliancePolicies', 'SecureScore', 'SecureScoreControlProfiles', 'Mailboxes', 'CASMailbox', 'MailboxPermissions', 'OneDriveUsage', 'MailboxUsage', 'Devices', 'AllRoles', 'Licenses', 'DeviceCompliancePolicies', - 'BitlockerKeys' + 'BitlockerKeys', 'Apps', 'ServicePrincipals' )] [string[]]$Types, diff --git a/Modules/CIPPCore/Public/Set-CIPPDefaultAPDeploymentProfile.ps1 b/Modules/CIPPCore/Public/Set-CIPPDefaultAPDeploymentProfile.ps1 index 7fa56473ec0b..770b24be8003 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDefaultAPDeploymentProfile.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDefaultAPDeploymentProfile.ps1 @@ -24,9 +24,7 @@ function Set-CIPPDefaultAPDeploymentProfile { # 'user-select' -> empty string (lets user choose during OOBE) # 'os-default' or $null -> $null (uses operating system default) # Specific tag (e.g. 'en-US') -> passed through as-is - if ($Language -eq 'user-select') { - $Language = '' - } elseif ($Language -eq 'os-default' -or $null -eq $Language) { + if ($Language -eq 'os-default') { $Language = $null } @@ -48,15 +46,19 @@ function Set-CIPPDefaultAPDeploymentProfile { 'displayName' = "$($DisplayName)" 'description' = "$($Description)" 'deviceNameTemplate' = "$($DeviceNameTemplate)" - 'locale' = $Language + 'locale' = "$($Language)" 'preprovisioningAllowed' = $([bool]($AllowWhiteGlove)) 'deviceType' = 'windowsPc' 'hardwareHashExtractionEnabled' = $([bool]($CollectHash)) 'roleScopeTagIds' = @() 'outOfBoxExperienceSetting' = $OutOfBoxSetting } + if ($Language -eq 'user-select') { + #Add language query to body only if user-select, as Graph API treats empty string differently than null + $ObjBody.locale = '' + $ObjBody | Add-Member -MemberType NoteProperty -Name 'language' -Value '' -Force + } $Body = ConvertTo-Json -InputObject $ObjBody -Depth 10 - Write-Information $Body $Profiles = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles' -tenantid $TenantFilter | Where-Object -Property displayName -EQ $DisplayName diff --git a/Modules/CIPPCore/Public/Set-CIPPOneDriveSharing.ps1 b/Modules/CIPPCore/Public/Set-CIPPOneDriveSharing.ps1 new file mode 100644 index 000000000000..8b0c8d4ed439 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPOneDriveSharing.ps1 @@ -0,0 +1,65 @@ +function Set-CIPPOneDriveSharing { + [CmdletBinding()] + param ( + $UserId, + $TenantFilter, + [ValidateSet('Disabled', 'ExternalUserSharingOnly', 'ExternalUserAndGuestSharing', 'ExistingExternalUserSharingOnly')] + [string]$SharingCapability = 'Disabled', + $APIName = 'Set OneDrive Sharing', + $Headers, + $URL + ) + + $SharingCapabilityMap = @{ + 'Disabled' = 0 + 'ExternalUserSharingOnly' = 1 + 'ExternalUserAndGuestSharing' = 2 + 'ExistingExternalUserSharingOnly' = 3 + } + $EnumValue = $SharingCapabilityMap[$SharingCapability] + + try { + if (!$URL) { + #Grab url, get root level, strip /documents + $URL = (New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/users/$($UserId)/drive" -asapp $true -tenantid $TenantFilter).webUrl -replace '/documents', '' + } + + $SharePointInfo = Get-SharePointAdminLink -Public $false -tenantFilter $TenantFilter + + $XML = @" + + + + $EnumValue + + + + + + + $URL + false + + + + + +"@ + $Request = New-GraphPostRequest -scope "$($SharePointInfo.AdminUrl)/.default" -tenantid $TenantFilter -Uri "$($SharePointInfo.AdminUrl)/_vti_bin/client.svc/ProcessQuery" -Type POST -Body $XML -ContentType 'text/xml' + + if (!$Request.ErrorInfo.ErrorMessage) { + $Message = "Successfully set OneDrive sharing to '$SharingCapability' for $UserId ($URL)" + Write-LogMessage -headers $Headers -API $APIName -message $Message -Sev Info -tenant $TenantFilter + return $Message + } else { + $Message = "Failed to set OneDrive sharing for $UserId : $($Request.ErrorInfo.ErrorMessage)" + Write-LogMessage -headers $Headers -API $APIName -message $Message -Sev Error -tenant $TenantFilter + return $Message + } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Message = "Failed to set OneDrive sharing for $UserId. Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -message $Message -Sev Error -tenant $TenantFilter -LogData $ErrorMessage + throw $Message + } +} diff --git a/Modules/CIPPCore/Public/Standards/Get-CIPPStandards.ps1 b/Modules/CIPPCore/Public/Standards/Get-CIPPStandards.ps1 index 71c231d4360c..f94b227967a0 100644 --- a/Modules/CIPPCore/Public/Standards/Get-CIPPStandards.ps1 +++ b/Modules/CIPPCore/Public/Standards/Get-CIPPStandards.ps1 @@ -22,6 +22,9 @@ function Get-CIPPStandards { # 1. Get all JSON-based templates from the "templates" table $Table = Get-CippTable -tablename 'templates' $Filter = "PartitionKey eq 'StandardsTemplateV2'" + # Always load ALL templates so the three-tier merge (AllTenants → Group → Tenant-Specific) + # 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 { @@ -31,7 +34,7 @@ function Get-CIPPStandards { } catch {} } | Where-Object { - $_.GUID -like $TemplateId -and $_.runManually -eq $runManually + $_.runManually -eq $runManually } # 1.5. Expand templates that contain TemplateList-Tags into multiple standards @@ -128,7 +131,7 @@ function Get-CIPPStandards { # 3. If -ListAllTenants, build standards for "AllTenants" only if ($ListAllTenants.IsPresent) { $AllTenantsTemplates = $Templates | Where-Object { - $_.tenantFilter.value -contains 'AllTenants' + $_.tenantFilter.value -contains 'AllTenants' -and $_.GUID -like $TemplateId } foreach ($Template in $AllTenantsTemplates) { @@ -505,6 +508,15 @@ function Get-CIPPStandards { $StandardName = $Key -replace '\|.*$', '' # Preserve TemplateId before removing $PreservedTemplateId = $Standard.TemplateId + + # When a specific TemplateId was requested, only emit standards that + # this template actually won after the three-tier merge. This prevents + # a group template manual run from executing standards that a + # tenant-specific template has overridden. + if ($TemplateId -ne '*' -and $PreservedTemplateId -notlike $TemplateId) { + continue + } + $Standard.PSObject.Properties.Remove('TemplateId') | Out-Null $Normalized = ConvertTo-CippStandardObject $Standard diff --git a/Modules/CIPPCore/Public/TenantGroups/Get-TenantGroups.ps1 b/Modules/CIPPCore/Public/TenantGroups/Get-TenantGroups.ps1 index 424891f4d555..96f70c91f1db 100644 --- a/Modules/CIPPCore/Public/TenantGroups/Get-TenantGroups.ps1 +++ b/Modules/CIPPCore/Public/TenantGroups/Get-TenantGroups.ps1 @@ -1,19 +1,3 @@ -if (-not $script:TenantGroupsCache) { - $script:TenantGroupsCache = @{ - Groups = $null - Members = $null - LastRefresh = $null - MembersByGroup = $null # Dictionary: GroupId -> members array - } -} - -# Result cache: keyed by "GroupId|TenantFilter|Dynamic" -if (-not $script:TenantGroupsResultCache) { - $script:TenantGroupsResultCache = @{} -} - -$script:TenantGroupsCacheTTL = (New-TimeSpan -Minutes 5) - function Get-TenantGroups { <# .SYNOPSIS @@ -35,6 +19,22 @@ function Get-TenantGroups { [switch]$Dynamic, [switch]$SkipCache ) + + if (-not $script:TenantGroupsCache) { + $script:TenantGroupsCache = @{ + Groups = $null + Members = $null + LastRefresh = $null + MembersByGroup = $null + } + } + if (-not $script:TenantGroupsResultCache) { + $script:TenantGroupsResultCache = @{} + } + if (-not $script:TenantGroupsCacheTTL) { + $script:TenantGroupsCacheTTL = (New-TimeSpan -Minutes 5) + } + $CacheKey = "$GroupId|$TenantFilter|$($Dynamic.IsPresent)" if ($SkipCache) { diff --git a/Modules/CIPPCore/Public/Test-CIPPAccessTenant.ps1 b/Modules/CIPPCore/Public/Test-CIPPAccessTenant.ps1 index c3eda8370f7f..66c987c0d00f 100644 --- a/Modules/CIPPCore/Public/Test-CIPPAccessTenant.ps1 +++ b/Modules/CIPPCore/Public/Test-CIPPAccessTenant.ps1 @@ -116,7 +116,7 @@ function Test-CIPPAccessTenant { $ExchangeLicenseCapable = $true $ExchangeSkippedByLicense = $false try { - $ExchangeLicenseCapable = Test-CIPPStandardLicense -StandardName 'ExchangeAccessCheck' -TenantFilter $Tenant.customerId -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') -SkipLog + $ExchangeLicenseCapable = Test-CIPPStandardLicense -StandardName 'ExchangeAccessCheck' -TenantFilter $Tenant.customerId -Preset Exchange -SkipLog } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -headers $Headers -API $APINAME -tenant $tenant.defaultDomainName -message "Exchange license capability check failed. Continuing with Exchange access validation. Error: $($ErrorMessage.NormalizedError)" -Sev 'Warning' -LogData $ErrorMessage diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheConditionalAccessPolicies.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheConditionalAccessPolicies.ps1 index bb516877c6c8..d5bd3ec19cde 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheConditionalAccessPolicies.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheConditionalAccessPolicies.ps1 @@ -17,7 +17,7 @@ function Set-CIPPDBCacheConditionalAccessPolicies { ) try { - $TestResult = Test-CIPPStandardLicense -StandardName 'ConditionalAccessCache' -TenantFilter $TenantFilter -RequiredCapabilities @('AAD_PREMIUM', 'AAD_PREMIUM_P2') -SkipLog + $TestResult = Test-CIPPStandardLicense -StandardName 'ConditionalAccessCache' -TenantFilter $TenantFilter -Preset Entra -SkipLog if ($TestResult -eq $false) { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Tenant does not have Azure AD Premium license, skipping CA' -sev Debug diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheDlpCompliancePolicies.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheDlpCompliancePolicies.ps1 index 52df23f538dd..776b868f163d 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheDlpCompliancePolicies.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheDlpCompliancePolicies.ps1 @@ -17,7 +17,7 @@ function Set-CIPPDBCacheDlpCompliancePolicies { ) try { - $LicenseCheck = Test-CIPPStandardLicense -StandardName 'DlpCompliancePoliciesCache' -TenantFilter $TenantFilter -RequiredCapabilities @('RMS_S_PREMIUM', 'RMS_S_PREMIUM2', 'MIP_S_CLP1', 'MIP_S_CLP2') -SkipLog + $LicenseCheck = Test-CIPPStandardLicense -StandardName 'DlpCompliancePoliciesCache' -TenantFilter $TenantFilter -Preset Compliance -SkipLog if ($LicenseCheck -eq $false) { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Tenant does not have a Purview/AIP license, skipping DLP compliance policies' -sev Debug diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoPresetSecurityPolicy.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoPresetSecurityPolicy.ps1 index 5476d515bbe9..0810cf69ade7 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoPresetSecurityPolicy.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoPresetSecurityPolicy.ps1 @@ -19,7 +19,7 @@ function Set-CIPPDBCacheExoPresetSecurityPolicy { try { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Preset Security Policies' -sev Debug - $MDOTestResult = Test-CIPPStandardLicense -StandardName 'ExoPresetSecurityPolicy' -TenantFilter $TenantFilter -RequiredCapabilities @('ATP_ENTERPRISE', 'ATP_ENTERPRISE_GOV', 'THREAT_INTELLIGENCE') -SkipLog + $MDOTestResult = Test-CIPPStandardLicense -StandardName 'ExoPresetSecurityPolicy' -TenantFilter $TenantFilter -Preset DefenderForOffice365 -SkipLog if ($MDOTestResult -eq $false) { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Skipping Preset Security Policy cache: tenant lacks Microsoft Defender for Office 365' -sev Debug return diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneAppProtectionPolicies.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneAppProtectionPolicies.ps1 index 97696ddf8b52..d3dd14ad33fa 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneAppProtectionPolicies.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneAppProtectionPolicies.ps1 @@ -7,7 +7,7 @@ function Set-CIPPDBCacheIntuneAppProtectionPolicies { ) try { - $TestResult = Test-CIPPStandardLicense -StandardName 'IntuneAppProtectionPoliciesCache' -TenantFilter $TenantFilter -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') -SkipLog + $TestResult = Test-CIPPStandardLicense -StandardName 'IntuneAppProtectionPoliciesCache' -TenantFilter $TenantFilter -Preset Intune -SkipLog if ($TestResult -eq $false) { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Tenant does not have Intune license, skipping app protection policies cache' -sev Debug return diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneApplications.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneApplications.ps1 index 35b802197442..811f2af821d3 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneApplications.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneApplications.ps1 @@ -7,7 +7,7 @@ function Set-CIPPDBCacheIntuneApplications { ) try { - $TestResult = Test-CIPPStandardLicense -StandardName 'IntuneApplicationsCache' -TenantFilter $TenantFilter -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') -SkipLog + $TestResult = Test-CIPPStandardLicense -StandardName 'IntuneApplicationsCache' -TenantFilter $TenantFilter -Preset Intune -SkipLog if ($TestResult -eq $false) { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Tenant does not have Intune license, skipping applications cache' -sev Debug return diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneAssignmentFilters.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneAssignmentFilters.ps1 index 6edfc3d9704e..882021cfb4ea 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneAssignmentFilters.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneAssignmentFilters.ps1 @@ -7,7 +7,7 @@ function Set-CIPPDBCacheIntuneAssignmentFilters { ) try { - $TestResult = Test-CIPPStandardLicense -StandardName 'IntuneAssignmentFiltersCache' -TenantFilter $TenantFilter -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') -SkipLog + $TestResult = Test-CIPPStandardLicense -StandardName 'IntuneAssignmentFiltersCache' -TenantFilter $TenantFilter -Preset Intune -SkipLog if ($TestResult -eq $false) { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Tenant does not have Intune license, skipping assignment filters cache' -sev Debug return diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneCompliancePolicies.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneCompliancePolicies.ps1 index 6f0a7770b308..80256d0ce92c 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneCompliancePolicies.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneCompliancePolicies.ps1 @@ -7,7 +7,7 @@ function Set-CIPPDBCacheIntuneCompliancePolicies { ) try { - $TestResult = Test-CIPPStandardLicense -StandardName 'IntuneCompliancePoliciesCache' -TenantFilter $TenantFilter -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') -SkipLog + $TestResult = Test-CIPPStandardLicense -StandardName 'IntuneCompliancePoliciesCache' -TenantFilter $TenantFilter -Preset Intune -SkipLog if ($TestResult -eq $false) { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Tenant does not have Intune license, skipping compliance policies cache' -sev Debug return diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntunePolicies.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntunePolicies.ps1 index f770d4c37cea..427965a7b3a0 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntunePolicies.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntunePolicies.ps1 @@ -17,7 +17,7 @@ function Set-CIPPDBCacheIntunePolicies { ) try { - $TestResult = Test-CIPPStandardLicense -StandardName 'IntunePoliciesCache' -TenantFilter $TenantFilter -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') -SkipLog + $TestResult = Test-CIPPStandardLicense -StandardName 'IntunePoliciesCache' -TenantFilter $TenantFilter -Preset Intune -SkipLog if ($TestResult -eq $false) { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Tenant does not have Intune license, skipping' -sev Debug diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneReusableSettings.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneReusableSettings.ps1 index e27fe5770dc4..77a964147403 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneReusableSettings.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneReusableSettings.ps1 @@ -7,7 +7,7 @@ function Set-CIPPDBCacheIntuneReusableSettings { ) try { - $TestResult = Test-CIPPStandardLicense -StandardName 'IntuneReusableSettingsCache' -TenantFilter $TenantFilter -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') -SkipLog + $TestResult = Test-CIPPStandardLicense -StandardName 'IntuneReusableSettingsCache' -TenantFilter $TenantFilter -Preset Intune -SkipLog if ($TestResult -eq $false) { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Tenant does not have Intune license, skipping reusable settings cache' -sev Debug return diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneScripts.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneScripts.ps1 index 47a074116886..8fb780b7d4ff 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneScripts.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntuneScripts.ps1 @@ -7,7 +7,7 @@ function Set-CIPPDBCacheIntuneScripts { ) try { - $TestResult = Test-CIPPStandardLicense -StandardName 'IntuneScriptsCache' -TenantFilter $TenantFilter -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') -SkipLog + $TestResult = Test-CIPPStandardLicense -StandardName 'IntuneScriptsCache' -TenantFilter $TenantFilter -Preset Intune -SkipLog if ($TestResult -eq $false) { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Tenant does not have Intune license, skipping scripts cache' -sev Debug return diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCachePIMSettings.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCachePIMSettings.ps1 index 768525f69e94..d5da952cec5c 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCachePIMSettings.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCachePIMSettings.ps1 @@ -17,7 +17,7 @@ function Set-CIPPDBCachePIMSettings { ) try { - $TestResult = Test-CIPPStandardLicense -StandardName 'PIMSettingsCache' -TenantFilter $TenantFilter -RequiredCapabilities @('AAD_PREMIUM_P2') -SkipLog + $TestResult = Test-CIPPStandardLicense -StandardName 'PIMSettingsCache' -TenantFilter $TenantFilter -Preset EntraP2 -SkipLog if ($TestResult -eq $false) { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Tenant does not have Azure AD Premium P2 license, skipping PIM' -sev Debug diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheSensitivityLabels.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheSensitivityLabels.ps1 index deafb9b25f9c..b9d89242742a 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheSensitivityLabels.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheSensitivityLabels.ps1 @@ -17,7 +17,7 @@ function Set-CIPPDBCacheSensitivityLabels { ) try { - $LicenseCheck = Test-CIPPStandardLicense -StandardName 'SensitivityLabelsCache' -TenantFilter $TenantFilter -RequiredCapabilities @('RMS_S_PREMIUM', 'RMS_S_PREMIUM2', 'MIP_S_CLP1', 'MIP_S_CLP2') -SkipLog + $LicenseCheck = Test-CIPPStandardLicense -StandardName 'SensitivityLabelsCache' -TenantFilter $TenantFilter -Preset Compliance -SkipLog if ($LicenseCheck -eq $false) { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Tenant does not have a Purview/AIP license, skipping sensitivity labels' -sev Debug diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheSharePointSiteUsage.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheSharePointSiteUsage.ps1 index 90ab81351b7a..5722f962b7a0 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheSharePointSiteUsage.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheSharePointSiteUsage.ps1 @@ -37,9 +37,17 @@ function Set-CIPPDBCacheSharePointSiteUsage { $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)) - $UsageRows = @(($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)) + $UsageRows = @(($UsageJson | ConvertFrom-Json).value) + } else { + $UsageRows = @($UsageBody.value) + } # Ensure a stable row key for usage rows. foreach ($UsageRow in $UsageRows) { diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheUsers.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheUsers.ps1 index e987260ab09b..e78271e0650f 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheUsers.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheUsers.ps1 @@ -19,7 +19,7 @@ function Set-CIPPDBCacheUsers { try { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching users' -sev Debug - $SignInLogsCapable = Test-CIPPStandardLicense -StandardName 'UserSignInLogsCapable' -TenantFilter $TenantFilter -RequiredCapabilities @('AAD_PREMIUM', 'AAD_PREMIUM_P2') -SkipLog + $SignInLogsCapable = Test-CIPPStandardLicense -StandardName 'UserSignInLogsCapable' -TenantFilter $TenantFilter -Preset Entra -SkipLog # Base properties needed by tests, standards, reports, UI, and integrations (Hudu, NinjaOne) $BaseSelect = @( 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 3b236cafbb4d..70304808efa0 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 @@ -11,10 +11,19 @@ function Invoke-ListFeatureFlags { try { Write-LogMessage -API 'ListFeatureFlags' -message 'Accessed feature flags list' -sev 'Debug' - $FeatureFlags = Get-CIPPFeatureFlag + $FeatureFlags = @(Get-CIPPFeatureFlag) + + # Environment-driven overrides: enable flags that depend on the runtime platform + if ($env:CIPPNG -eq 'true') { + foreach ($Flag in $FeatureFlags) { + if ($Flag.Id -eq 'SuperAdminNG') { + $Flag.Enabled = $true + } + } + } $StatusCode = [HttpStatusCode]::OK - $Body = @($FeatureFlags) + $Body = $FeatureFlags } catch { Write-LogMessage -API 'ListFeatureFlags' -message "Failed to retrieve feature flags: $($_.Exception.Message)" -sev 'Error' $StatusCode = [HttpStatusCode]::InternalServerError 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 new file mode 100644 index 000000000000..1220df4bad43 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCIPPUsers.ps1 @@ -0,0 +1,114 @@ +function Invoke-ExecCIPPUsers { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + CIPP.SuperAdmin.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + $Action = $Request.Query.Action ?? $Request.Body.Action + $Table = Get-CippTable -tablename 'allowedUsers' + + switch ($Action) { + 'AddUpdate' { + try { + $UPN = $Request.Body.UPN + if ([string]::IsNullOrWhiteSpace($UPN)) { + throw 'UPN (email) is required' + } + $UPN = $UPN.Trim() + + $Roles = @($Request.Body.Roles) + if ($Roles.Count -eq 0) { + throw 'At least one role must be assigned' + } + + # Validate roles exist (built-in + custom) + $CippRolesJson = Join-Path -Path $env:CIPPRootPath -ChildPath 'Config\cipp-roles.json' + $BuiltInRoles = if (Test-Path $CippRolesJson) { + ([System.IO.File]::ReadAllText($CippRolesJson) | ConvertFrom-Json).PSObject.Properties.Name + } else { + @('readonly', 'editor', 'admin', 'superadmin') + } + + $CustomRolesTable = Get-CippTable -tablename 'CustomRoles' + $CustomRoles = @((Get-CIPPAzDataTableEntity @CustomRolesTable).RowKey) + $AllValidRoles = @($BuiltInRoles) + @($CustomRoles) + @('anonymous', 'authenticated') + + foreach ($Role in $Roles) { + if ($Role -notin $AllValidRoles) { + throw "Invalid role: $Role. Valid roles: $($AllValidRoles -join ', ')" + } + } + + $Entity = @{ + PartitionKey = 'User' + RowKey = $UPN + Roles = [string](@($Roles) | ConvertTo-Json -Compress -AsArray) + } + Add-CIPPAzDataTableEntity @Table -Entity $Entity -Force | Out-Null + + # Invalidate the in-memory user cache so changes apply immediately + try { [Craft.Services.AuthBridge]::InvalidateUsers() } catch {} + + $Result = "Successfully added/updated user $UPN with roles: $($Roles -join ', ')" + Write-LogMessage -API $APIName -headers $Headers -message $Result -sev Info + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API $APIName -headers $Headers -message "Failed to add/update user: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = @{ Results = "Failed: $($ErrorMessage.NormalizedError)" } + } + } + } + 'Delete' { + try { + $UPN = $Request.Body.UPN + if ([string]::IsNullOrWhiteSpace($UPN)) { + throw 'UPN (email) is required' + } + $UPN = $UPN.Trim() + + # Self-lockout protection: prevent removing yourself + $CurrentUser = $Request.Headers.'x-ms-client-principal-name' + if ($CurrentUser -and $UPN -ieq $CurrentUser) { + throw 'Cannot remove your own user account. This would lock you out.' + } + + $ExistingEntity = Get-CIPPAzDataTableEntity @Table -Filter "RowKey eq '$UPN'" + if (-not $ExistingEntity) { + throw "User $UPN not found in the allowed users table" + } + + Remove-AzDataTableEntity -Force @Table -Entity $ExistingEntity + try { [Craft.Services.AuthBridge]::InvalidateUsers() } catch {} + + $Result = "Successfully removed user $UPN" + Write-LogMessage -API $APIName -headers $Headers -message $Result -sev Info + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API $APIName -headers $Headers -message "Failed to delete user: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = @{ Results = "Failed: $($ErrorMessage.NormalizedError)" } + } + } + } + default { + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = @{ Results = "Unknown action: $Action. Valid actions: AddUpdate, Delete" } + } + } + } + + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = @{ Results = $Result } + } +} 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 new file mode 100644 index 000000000000..be842ac61a90 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecContainerManagement.ps1 @@ -0,0 +1,351 @@ +function Invoke-ExecContainerManagement { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + CIPP.SuperAdmin.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + $Action = $Request.Query.Action ?? $Request.Body.Action + + $ValidChannels = @('latest', 'dev', 'nightly') + $SettingsTable = Get-CippTable -tablename 'ContainerUpdateSettings' + + # Helper: resolve ARM site details + function Get-ContainerSiteInfo { + $info = @{ + Subscription = Get-CIPPAzFunctionAppSubId + SiteName = $env:WEBSITE_SITE_NAME + RGName = $env:WEBSITE_RESOURCE_GROUP + } + if (-not $info.RGName) { + $Owner = $env:WEBSITE_OWNER_NAME + if ($Owner -match '^(?[^+]+)\+(?[^-]+(?:-[^-]+)*?)(?:-[^-]+webspace(?:-Linux)?)?$') { + $info.RGName = $Matches.RGName + } + } + return $info + } + + # Helper: query GHCR for the image digest of a given tag + function Get-GHCRImageDigest { + param([string]$ImageRef, [string]$Tag) + + # Parse image reference: ghcr.io/owner/repo or owner/repo + $imagePath = $ImageRef -replace '^ghcr\.io/', '' -replace ':.*$', '' + if (-not $imagePath) { throw 'Could not parse image path from reference' } + + # Get anonymous token for GHCR (public packages) + $tokenUri = "https://ghcr.io/token?scope=repository:${imagePath}:pull" + $tokenResp = Invoke-RestMethod -Uri $tokenUri -Method GET -ErrorAction Stop + $token = $tokenResp.token + + # Get manifest digest via HEAD request + $manifestUri = "https://ghcr.io/v2/$imagePath/manifests/$Tag" + $digestHeaders = @{ + Authorization = "Bearer $token" + Accept = 'application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.docker.distribution.manifest.v2+json' + } + $resp = Invoke-WebRequest -Uri $manifestUri -Method HEAD -Headers $digestHeaders -ErrorAction Stop + $digest = $resp.Headers['Docker-Content-Digest'] + if ($digest -is [array]) { $digest = $digest[0] } + return [string]$digest + } + + switch ($Action) { + 'Status' { + try { + $CurrentVersion = $env:APP_VERSION ?? 'unknown' + $CommitSha = $env:COMMIT_SHA ?? 'unknown' + $ImageTag = $env:IMAGE_TAG ?? 'unknown' + $CurrentChannel = $ImageTag + + # Read the full container image reference from ARM + $CurrentImage = 'unknown' + $ConfiguredChannel = $CurrentChannel + $site = Get-ContainerSiteInfo + if ($site.Subscription -and $site.RGName -and $site.SiteName) { + try { + $apiVersion = '2024-11-01' + $uri = "https://management.azure.com/subscriptions/$($site.Subscription)/resourceGroups/$($site.RGName)/providers/Microsoft.Web/sites/$($site.SiteName)/config/web?api-version=$apiVersion" + $webConfig = New-CIPPAzRestRequest -Uri $uri -Method GET + $linuxFxVersion = $webConfig.properties.linuxFxVersion + if ($linuxFxVersion) { + $CurrentImage = $linuxFxVersion -replace '^DOCKER\|', '' + if ($CurrentImage -match ':([^:]+)$') { + $ConfiguredChannel = $Matches[1] + } + } + } catch { + Write-Information "Could not read container config from ARM: $_" + } + } + + # Read update settings and last check result + $Settings = Get-CIPPAzDataTableEntity @SettingsTable -Filter "PartitionKey eq 'Settings' and RowKey eq 'UpdateConfig'" | Select-Object -First 1 + $UpdateInfo = @{ + AutoUpdate = $false + CheckInterval = '0' + CheckTime = $null + LastCheck = $null + UpdateAvailable = $false + RunningDigest = $null + RemoteDigest = $null + } + if ($Settings) { + $UpdateInfo.AutoUpdate = $Settings.AutoUpdate -eq 'true' + $UpdateInfo.CheckInterval = $Settings.CheckInterval ?? '0' + $UpdateInfo.CheckTime = $Settings.CheckTime ?? $null + $UpdateInfo.LastCheck = if ($Settings.LastCheck) { [int64]$Settings.LastCheck } else { $null } + $UpdateInfo.UpdateAvailable = $Settings.UpdateAvailable -eq 'true' + $UpdateInfo.RunningDigest = $Settings.RunningDigest ?? $null + $UpdateInfo.RemoteDigest = $Settings.RemoteDigest ?? $null + } + + $Body = @{ + Results = @{ + CurrentVersion = $CurrentVersion + CommitSha = $CommitSha + ImageTag = $ImageTag + CurrentChannel = $CurrentChannel + ConfiguredChannel = $ConfiguredChannel + CurrentImage = $CurrentImage + SiteName = $site.SiteName + ValidChannels = $ValidChannels + UpdateSettings = $UpdateInfo + } + } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API $APIName -headers $Headers -message "Failed to get container status: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::InternalServerError + Body = @{ Results = "Failed: $($ErrorMessage.NormalizedError)" } + } + } + } + 'CheckUpdate' { + try { + $site = Get-ContainerSiteInfo + $ImageTag = $env:IMAGE_TAG ?? 'unknown' + + # Get the current image reference from ARM + $CurrentImage = $null + if ($site.Subscription -and $site.RGName -and $site.SiteName) { + $apiVersion = '2024-11-01' + $uri = "https://management.azure.com/subscriptions/$($site.Subscription)/resourceGroups/$($site.RGName)/providers/Microsoft.Web/sites/$($site.SiteName)/config/web?api-version=$apiVersion" + $webConfig = New-CIPPAzRestRequest -Uri $uri -Method GET + $linuxFxVersion = $webConfig.properties.linuxFxVersion + if ($linuxFxVersion) { + $CurrentImage = $linuxFxVersion -replace '^DOCKER\|', '' + } + } + if (-not $CurrentImage) { + throw 'Could not determine current container image from ARM config' + } + + # Update checking only works with GHCR-hosted images + if ($CurrentImage -notmatch '^ghcr\.io/') { + $Body = @{ + Results = @{ + Message = "Update checking is only supported for GHCR-hosted images. Current image: $CurrentImage" + UpdateAvailable = $false + RunningDigest = $null + RemoteDigest = $null + CheckedTag = $null + } + } + break + } + + # Determine the tag to check + $CheckTag = if ($CurrentImage -match ':([^:]+)$') { $Matches[1] } else { $ImageTag } + + # Query GHCR for the remote digest + $RemoteDigest = Get-GHCRImageDigest -ImageRef $CurrentImage -Tag $CheckTag + + # Get the running container's digest — query for the baked-in tag to get what we're running + $RunningDigest = $null + try { + $RunningDigest = Get-GHCRImageDigest -ImageRef $CurrentImage -Tag $ImageTag + } catch { + Write-Information "Could not get running digest for tag $ImageTag — may be first check" + } + + $UpdateAvailable = $false + if ($RemoteDigest -and $RunningDigest -and $RemoteDigest -ne $RunningDigest) { + $UpdateAvailable = $true + } + + # Store result + $Entity = @{ + PartitionKey = 'Settings' + RowKey = 'UpdateConfig' + LastCheck = [string][int64](([DateTimeOffset]::UtcNow).ToUnixTimeSeconds()) + UpdateAvailable = [string]$UpdateAvailable + RunningDigest = [string]($RunningDigest ?? '') + RemoteDigest = [string]($RemoteDigest ?? '') + } + # Merge with existing settings (preserve AutoUpdate, CheckInterval, CheckTime) + $Existing = Get-CIPPAzDataTableEntity @SettingsTable -Filter "PartitionKey eq 'Settings' and RowKey eq 'UpdateConfig'" | Select-Object -First 1 + if ($Existing) { + $Entity.AutoUpdate = $Existing.AutoUpdate ?? 'false' + $Entity.CheckInterval = $Existing.CheckInterval ?? '0' + $Entity.CheckTime = $Existing.CheckTime ?? '' + } + Add-CIPPAzDataTableEntity @SettingsTable -Entity $Entity -Force | Out-Null + + # Auto-restart if enabled and update is available + $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 {} + $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." + Write-LogMessage -API $APIName -headers $Headers -message "Container update available (running: $RunningDigest, remote: $RemoteDigest)" -sev Info + } else { + $Result = "Container is up to date. Digest: $RunningDigest" + } + $Body = @{ + Results = @{ + Message = $Result + UpdateAvailable = $UpdateAvailable + RunningDigest = $RunningDigest + RemoteDigest = $RemoteDigest + CheckedTag = $CheckTag + } + } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API $APIName -headers $Headers -message "Failed to check for update: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::InternalServerError + Body = @{ Results = "Failed: $($ErrorMessage.NormalizedError)" } + } + } + } + 'SaveUpdateSettings' { + try { + $AutoUpdate = [bool]($Request.Body.AutoUpdate) + $CheckInterval = $Request.Body.CheckInterval ?? '0' + $CheckTime = $Request.Body.CheckTime + $ValidIntervals = @('0', '1h', '4h', '12h', '1d') + if ($CheckInterval -notin $ValidIntervals) { + throw "Invalid check interval: $CheckInterval. Valid: $($ValidIntervals -join ', ')" + } + if ($CheckTime -and ($CheckTime -lt 0 -or $CheckTime -gt 23)) { + throw "Invalid check time: $CheckTime. Must be 0-23 (UTC hour)." + } + + # Read existing settings to preserve check results + $Existing = Get-CIPPAzDataTableEntity @SettingsTable -Filter "PartitionKey eq 'Settings' and RowKey eq 'UpdateConfig'" | Select-Object -First 1 + $Entity = @{ + PartitionKey = 'Settings' + RowKey = 'UpdateConfig' + AutoUpdate = [string]$AutoUpdate + CheckInterval = [string]$CheckInterval + CheckTime = [string]($CheckTime ?? '') + LastCheck = [string]($Existing.LastCheck ?? '') + UpdateAvailable = [string]($Existing.UpdateAvailable ?? 'false') + RunningDigest = [string]($Existing.RunningDigest ?? '') + RemoteDigest = [string]($Existing.RemoteDigest ?? '') + } + Add-CIPPAzDataTableEntity @SettingsTable -Entity $Entity -Force | Out-Null + + $IntervalLabel = if ($CheckInterval -eq '0') { 'disabled' } else { "every $CheckInterval" } + $AutoLabel = if ($AutoUpdate) { 'auto-restart enabled' } else { 'manual restart' } + $TimeLabel = if ($CheckTime -and $CheckInterval -ne '0') { " at ${CheckTime}:00 UTC" } else { '' } + $Result = "Update settings saved. Check interval: ${IntervalLabel}${TimeLabel}, $AutoLabel." + Write-LogMessage -API $APIName -headers $Headers -message $Result -sev Info + $Body = @{ Results = $Result } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API $APIName -headers $Headers -message "Failed to save update settings: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = @{ Results = "Failed: $($ErrorMessage.NormalizedError)" } + } + } + } + 'UpdateChannel' { + try { + $NewChannel = $Request.Body.Channel + if ([string]::IsNullOrWhiteSpace($NewChannel)) { + throw 'Channel is required' + } + if ($NewChannel -notin $ValidChannels) { + throw "Invalid channel: $NewChannel. Valid channels: $($ValidChannels -join ', ')" + } + + $site = Get-ContainerSiteInfo + if (-not ($site.Subscription -and $site.RGName -and $site.SiteName)) { + throw 'Could not determine Azure App Service details from environment' + } + + $apiVersion = '2024-11-01' + $getUri = "https://management.azure.com/subscriptions/$($site.Subscription)/resourceGroups/$($site.RGName)/providers/Microsoft.Web/sites/$($site.SiteName)/config/web?api-version=$apiVersion" + $webConfig = New-CIPPAzRestRequest -Uri $getUri -Method GET + $currentLinuxFx = $webConfig.properties.linuxFxVersion + if (-not $currentLinuxFx) { + throw 'Could not read current linuxFxVersion — is this a Linux container app?' + } + + $currentImage = $currentLinuxFx -replace '^DOCKER\|', '' + if ($currentImage -match '^(.+):([^:]+)$') { + $imageBase = $Matches[1] + $newLinuxFx = "DOCKER|${imageBase}:${NewChannel}" + } else { + $newLinuxFx = "DOCKER|${currentImage}:${NewChannel}" + } + + $putBody = @{ properties = @{ linuxFxVersion = $newLinuxFx } } + New-CIPPAzRestRequest -Uri $getUri -Method PATCH -Body $putBody -ContentType 'application/json' | Out-Null + + $Result = "Release channel updated to '$NewChannel'. Image: $newLinuxFx. The container will pull the new image on next restart." + Write-LogMessage -API $APIName -headers $Headers -message "Release channel changed to $NewChannel ($newLinuxFx)" -sev Info + $Body = @{ Results = $Result } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API $APIName -headers $Headers -message "Failed to update channel: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = @{ Results = "Failed: $($ErrorMessage.NormalizedError)" } + } + } + } + 'Restart' { + try { + 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') + } catch { + $Body = @{ Results = 'Restart command sent but the bridge is not available. The app may need to be restarted from the Azure Portal.' } + } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API $APIName -headers $Headers -message "Failed to restart: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::InternalServerError + Body = @{ Results = "Failed: $($ErrorMessage.NormalizedError)" } + } + } + } + default { + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = @{ Results = "Unknown action: $Action. Valid actions: Status, CheckUpdate, SaveUpdateSettings, UpdateChannel, Restart" } + } + } + } + + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = $Body + } +} 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 new file mode 100644 index 000000000000..5ed1dff766e2 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListCIPPUsers.ps1 @@ -0,0 +1,80 @@ +function Invoke-ListCIPPUsers { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + CIPP.SuperAdmin.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Table = Get-CippTable -tablename 'allowedUsers' + + try { + # Get all users from the allowedUsers table + $Users = Get-CIPPAzDataTableEntity @Table | Where-Object { -not $_.RowKey.StartsWith('_') } + + # Get available roles (built-in + custom) + $CippRolesJson = Join-Path -Path $env:CIPPRootPath -ChildPath 'Config\cipp-roles.json' + $BuiltInRoles = if (Test-Path $CippRolesJson) { + ([System.IO.File]::ReadAllText($CippRolesJson) | ConvertFrom-Json).PSObject.Properties.Name + } else { + @('readonly', 'editor', 'admin', 'superadmin') + } + + $CustomRolesTable = Get-CippTable -tablename 'CustomRoles' + $CustomRoleEntities = Get-CIPPAzDataTableEntity @CustomRolesTable + $CustomRoleNames = @($CustomRoleEntities | ForEach-Object { $_.RowKey } | Where-Object { $_ }) + + # Build user list with parsed roles + $UserList = [System.Collections.Generic.List[pscustomobject]]::new() + foreach ($User in $Users) { + $ParsedRoles = @() + if ($User.Roles) { + try { + $ParsedRoles = @($User.Roles | ConvertFrom-Json -ErrorAction Stop) + } catch { + $ParsedRoles = @($User.Roles) + } + } + + $UserList.Add([pscustomobject]@{ + UPN = $User.RowKey + Roles = $ParsedRoles + }) + } + + # Build available roles list for frontend dropdown + $AvailableRoles = [System.Collections.Generic.List[pscustomobject]]::new() + foreach ($Role in $BuiltInRoles) { + $AvailableRoles.Add([pscustomobject]@{ + RoleName = $Role + Type = 'Built-In' + }) + } + foreach ($Role in $CustomRoleNames) { + $AvailableRoles.Add([pscustomobject]@{ + RoleName = $Role + Type = 'Custom' + }) + } + + $Body = @{ + Users = @($UserList) + AvailableRoles = @($AvailableRoles) + } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API $APIName -message "Failed to list CIPP users: $($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/CIPP/Settings/Invoke-ListContainerLogs.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListContainerLogs.ps1 new file mode 100644 index 000000000000..a98c001c05b8 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListContainerLogs.ps1 @@ -0,0 +1,300 @@ +function Invoke-ListContainerLogs { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + CIPP.SuperAdmin.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Action = $Request.Query.Action ?? 'ReadLog' + + # ── Helper: parse raw log lines into structured objects ── + function ConvertTo-LogEntry { + param([string[]]$Lines) + foreach ($Line in $Lines) { + if ([string]::IsNullOrWhiteSpace($Line)) { continue } + if ($Line.Length -gt 0 -and [char]::IsWhiteSpace($Line[0])) { continue } + # ISO 8601: "2026-05-13T10:30:00.000Z [INF] message" + if ($Line -match '^\s*(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)\s+\[(\w+)\]\s+(.*)$') { + [PSCustomObject]@{ + Timestamp = $Matches[1] + Level = $Matches[2] + Message = $Matches[3] + Raw = $Line + } + # Legacy: "2026-05-13 10:30:00.000 [INF] message" + } elseif ($Line -match '^\s*(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d{3})\s+\[(\w+)\]\s+(.*)$') { + [PSCustomObject]@{ + Timestamp = "$($Matches[1].Replace(' ', 'T'))Z" + Level = $Matches[2] + Message = $Matches[3] + Raw = $Line + } + } else { + [PSCustomObject]@{ + Timestamp = '' + Level = '' + Message = $Line + Raw = $Line + } + } + } + } + + # ── Helper: parse a KQL-subset query into LogBridge parameters ── + # Supported syntax: + # where Level == "ERR" + # where Level in ("ERR", "CRT", "WRN") + # where Message contains "timeout" + # where Message !contains "heartbeat" + # where Message matches regex "error|fail" + # where Timestamp > ago(1h) + # where Timestamp > ago(30m) + # where Timestamp > ago(2d) + # where Timestamp between (ago(2h) .. ago(1h)) + # where Timestamp > datetime(2026-05-14 10:00) + # take 500 + # sort by Timestamp desc + # sort by Timestamp asc + function ConvertFrom-LogQuery { + param([string]$Query) + + $params = @{ + Tail = 500 + Level = $null + Search = $null + Exclude = $null + RegexPattern = $null + From = $null + To = $null + File = $null + SearchAll = $false + SortNewest = $true + } + + # Split on pipe, trim each clause + $clauses = $Query -split '\|' | ForEach-Object { $_.Trim() } | Where-Object { $_ } + + foreach ($clause in $clauses) { + # take N + if ($clause -match '^\s*take\s+(\d+)\s*$') { + $params.Tail = [int]$Matches[1] + continue + } + + # sort by Timestamp asc/desc + if ($clause -match '^\s*sort\s+by\s+\w+\s+(asc|desc)\s*$') { + $params.SortNewest = ($Matches[1] -eq 'desc') + continue + } + + # search all files + if ($clause -match '^\s*search\s+all(\s+files)?\s*$') { + $params.SearchAll = $true + continue + } + + # where clauses + if ($clause -match '^\s*where\s+(.+)$') { + $condition = $Matches[1].Trim() + + # Level == "X" + if ($condition -match '^Level\s*==\s*"(\w+)"\s*$') { + $params.Level = $Matches[1] + } + # Level in ("X", "Y", ...) + elseif ($condition -match '^Level\s+in\s*\(\s*(.+)\s*\)\s*$') { + $items = $Matches[1] -split ',' | ForEach-Object { $_.Trim().Trim('"', "'", ' ') } | Where-Object { $_ } + $params.Level = $items -join ',' + } + # Level != "X" + elseif ($condition -match '^Level\s*!=\s*"(\w+)"\s*$') { + # Exclude this level by including all others + $allLevels = @('TRC', 'DBG', 'INF', 'WRN', 'ERR', 'CRT') + $excluded = $Matches[1].ToUpper() + $params.Level = ($allLevels | Where-Object { $_ -ne $excluded }) -join ',' + } + # Message contains "text" + elseif ($condition -match '^Message\s+contains\s+"(.+)"\s*$') { + $params.Search = $Matches[1] + } + # Message !contains "text" + elseif ($condition -match '^Message\s+!contains\s+"(.+)"\s*$') { + $params.Exclude = $Matches[1] + } + # Message matches regex "pattern" + elseif ($condition -match '^Message\s+matches\s+regex\s+"(.+)"\s*$') { + $params.RegexPattern = $Matches[1] + } + # Timestamp > ago(Xh/m/d/s) + elseif ($condition -match '^Timestamp\s*>\s*ago\((\d+)([smhdw])\)\s*$') { + $amount = [int]$Matches[1] + $unit = $Matches[2] + $params.From = switch ($unit) { + 's' { [DateTime]::UtcNow.AddSeconds(-$amount) } + 'm' { [DateTime]::UtcNow.AddMinutes(-$amount) } + 'h' { [DateTime]::UtcNow.AddHours(-$amount) } + 'd' { [DateTime]::UtcNow.AddDays(-$amount) } + 'w' { [DateTime]::UtcNow.AddDays(-$amount * 7) } + } + } + # Timestamp between (ago(Xh) .. ago(Yh)) + elseif ($condition -match '^Timestamp\s+between\s*\(\s*ago\((\d+)([smhdw])\)\s*\.\.\s*ago\((\d+)([smhdw])\)\s*\)\s*$') { + $fromAmount = [int]$Matches[1]; $fromUnit = $Matches[2] + $toAmount = [int]$Matches[3]; $toUnit = $Matches[4] + $params.From = switch ($fromUnit) { + 's' { [DateTime]::UtcNow.AddSeconds(-$fromAmount) } + 'm' { [DateTime]::UtcNow.AddMinutes(-$fromAmount) } + 'h' { [DateTime]::UtcNow.AddHours(-$fromAmount) } + 'd' { [DateTime]::UtcNow.AddDays(-$fromAmount) } + 'w' { [DateTime]::UtcNow.AddDays(-$fromAmount * 7) } + } + $params.To = switch ($toUnit) { + 's' { [DateTime]::UtcNow.AddSeconds(-$toAmount) } + 'm' { [DateTime]::UtcNow.AddMinutes(-$toAmount) } + 'h' { [DateTime]::UtcNow.AddHours(-$toAmount) } + 'd' { [DateTime]::UtcNow.AddDays(-$toAmount) } + 'w' { [DateTime]::UtcNow.AddDays(-$toAmount * 7) } + } + } + # Timestamp between (ago(Xh) .. now()) + elseif ($condition -match '^Timestamp\s+between\s*\(\s*ago\((\d+)([smhdw])\)\s*\.\.\s*now\(\)\s*\)\s*$') { + $amount = [int]$Matches[1]; $unit = $Matches[2] + $params.From = switch ($unit) { + 's' { [DateTime]::UtcNow.AddSeconds(-$amount) } + 'm' { [DateTime]::UtcNow.AddMinutes(-$amount) } + 'h' { [DateTime]::UtcNow.AddHours(-$amount) } + 'd' { [DateTime]::UtcNow.AddDays(-$amount) } + 'w' { [DateTime]::UtcNow.AddDays(-$amount * 7) } + } + } + # Timestamp > datetime(2026-05-14) or datetime(2026-05-14 10:00) + elseif ($condition -match '^Timestamp\s*>\s*datetime\((.+)\)\s*$') { + $params.From = [DateTime]::Parse($Matches[1]).ToUniversalTime() + } + # Timestamp < datetime(...) + elseif ($condition -match '^Timestamp\s*<\s*datetime\((.+)\)\s*$') { + $params.To = [DateTime]::Parse($Matches[1]).ToUniversalTime() + } + # Unrecognized where clause — ignore + } + # Unrecognized clause — ignore + } + + return $params + } + + try { + switch ($Action) { + 'ListFiles' { + $Results = [Craft.Services.LogBridge]::GetLogFiles() + $Body = @{ Results = @($Results) } + } + 'ReadLog' { + $Tail = [int]($Request.Query.Tail ?? '500') + $Level = $Request.Query.Level + $Search = $Request.Query.Search + $File = $Request.Query.File + $Exclude = $Request.Query.Exclude + $Regex = $Request.Query.Regex + $SortDesc = $Request.Query.SortDesc + + $From = $null + $To = $null + if ($Request.Query.From) { $From = [DateTime]::Parse($Request.Query.From).ToUniversalTime() } + if ($Request.Query.To) { $To = [DateTime]::Parse($Request.Query.To).ToUniversalTime() } + + $LevelParam = if ([string]::IsNullOrEmpty($Level)) { $null } else { $Level } + $SearchParam = if ([string]::IsNullOrEmpty($Search)) { $null } else { $Search } + $FileParam = if ([string]::IsNullOrEmpty($File)) { $null } else { $File } + $ExcludeParam = if ([string]::IsNullOrEmpty($Exclude)) { $null } else { $Exclude } + $RegexParam = if ([string]::IsNullOrEmpty($Regex)) { $null } else { $Regex } + $SortNewest = $SortDesc -eq 'true' + + $Lines = [Craft.Services.LogBridge]::ReadLog($Tail, $LevelParam, $SearchParam, $FileParam, $From, $To, $ExcludeParam, $RegexParam, $SortNewest) + $Results = ConvertTo-LogEntry -Lines $Lines + $Body = @{ Results = @($Results) } + } + 'SearchAll' { + $Search = $Request.Query.Search + $Level = $Request.Query.Level + $Tail = [int]($Request.Query.Tail ?? '500') + $Exclude = $Request.Query.Exclude + $Regex = $Request.Query.Regex + $SortDesc = $Request.Query.SortDesc + + $From = $null + $To = $null + if ($Request.Query.From) { $From = [DateTime]::Parse($Request.Query.From).ToUniversalTime() } + if ($Request.Query.To) { $To = [DateTime]::Parse($Request.Query.To).ToUniversalTime() } + + $SearchParam = if ([string]::IsNullOrEmpty($Search)) { $null } else { $Search } + $LevelParam = if ([string]::IsNullOrEmpty($Level)) { $null } else { $Level } + $ExcludeParam = if ([string]::IsNullOrEmpty($Exclude)) { $null } else { $Exclude } + $RegexParam = if ([string]::IsNullOrEmpty($Regex)) { $null } else { $Regex } + $SortNewest = $SortDesc -eq 'true' + + $Lines = [Craft.Services.LogBridge]::SearchAllFiles($SearchParam, $LevelParam, $From, $To, $Tail, $ExcludeParam, $RegexParam, $SortNewest) + $Results = ConvertTo-LogEntry -Lines $Lines + $Body = @{ Results = @($Results) } + } + 'Query' { + $Query = $Request.Query.Query ?? $Request.Body.Query + if ([string]::IsNullOrWhiteSpace($Query)) { + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = @{ Results = 'Query parameter is required' } + } + } + + $p = ConvertFrom-LogQuery -Query $Query + + $LevelParam = if ([string]::IsNullOrEmpty($p.Level)) { $null } else { $p.Level } + $SearchParam = if ([string]::IsNullOrEmpty($p.Search)) { $null } else { $p.Search } + $ExcludeParam = if ([string]::IsNullOrEmpty($p.Exclude)) { $null } else { $p.Exclude } + $RegexParam = if ([string]::IsNullOrEmpty($p.RegexPattern)) { $null } else { $p.RegexPattern } + $FileParam = if ([string]::IsNullOrEmpty($p.File)) { $null } else { $p.File } + + if ($p.SearchAll) { + $Lines = [Craft.Services.LogBridge]::SearchAllFiles($SearchParam, $LevelParam, $p.From, $p.To, $p.Tail, $ExcludeParam, $RegexParam, $p.SortNewest) + } else { + $Lines = [Craft.Services.LogBridge]::ReadLog($p.Tail, $LevelParam, $SearchParam, $FileParam, $p.From, $p.To, $ExcludeParam, $RegexParam, $p.SortNewest) + } + + $Results = ConvertTo-LogEntry -Lines $Lines + $Body = @{ Results = @($Results) } + } + 'GetInfo' { + $Body = @{ + Results = @{ + CurrentFile = [Craft.Services.LogBridge]::GetCurrentLogPath() + LogDirectory = [Craft.Services.LogBridge]::GetLogDirectory() + Files = @([Craft.Services.LogBridge]::GetLogFiles()) + } + } + } + default { + $Body = @{ Results = "Unknown action: $Action" } + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = $Body + } + } + } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API $APIName -message "Container logs 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/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/CIPP/Setup/Invoke-ExecSSOSetup.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecSSOSetup.ps1 new file mode 100644 index 000000000000..a0d29329799d --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecSSOSetup.ps1 @@ -0,0 +1,531 @@ +function Invoke-ExecSSOSetup { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + CIPP.AppSettings.ReadWrite + #> + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + $Action = $Request.Body.Action ?? $Request.Query.Action ?? 'Status' + $MigrationTable = Get-CIPPTable -tablename 'SSOMigration' + + switch ($Action) { + 'Status' { + # Read live EasyAuth config from the platform-injected env var when available + if ($env:CIPPNG) { + try { + $EasyAuthEnabled = $env:WEBSITE_AUTH_ENABLED -eq 'True' + $ConfigJson = $env:WEBSITE_AUTH_V2_CONFIG_JSON + if ($EasyAuthEnabled -and $ConfigJson) { + $Config = $ConfigJson | ConvertFrom-Json -ErrorAction Stop + $AAD = $Config.identityProviders.azureActiveDirectory + $Issuer = $AAD.registration.openIdIssuer ?? '' + $ClientId = $AAD.registration.clientId ?? '' + $IsMultiTenant = $Issuer -match '/common/' + $IssuerTenantId = if (-not $IsMultiTenant -and $Issuer -match 'microsoftonline\.com/([^/]+)/') { $Matches[1] } else { $null } + $AllowedAudiences = @($AAD.validation.allowedAudiences) + $AllowedApps = @($AAD.validation.defaultAuthorizationPolicy.allowedApplications) + $ExcludedPaths = @($Config.globalValidation.excludedPaths) + + $Body = @{ + Results = @{ + configured = $true + status = 'complete' + appId = $ClientId + multiTenant = $IsMultiTenant + tenantId = $IssuerTenantId + issuer = $Issuer + audiences = $AllowedAudiences + allowedApps = $AllowedApps + excludedPaths = $ExcludedPaths + easyAuthActive = $true + } + } + } 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 } } + } + } else { + # Otherwise read from migration table + try { + $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') + createdAt = $Migration.CreatedAt + lastChecked = $Migration.LastChecked + lastError = $Migration.LastError + } + } + } else { + $Body = @{ Results = @{ configured = $false; status = 'none' } } + } + } 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 } } + } + } + } + + '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)" + } + + 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') { + $Body = @{ + Results = @{ + message = 'SSO migration already completed.' + appId = $Existing.AppId + severity = 'info' + } + } + break + } + + # If we have an existing record that isn't complete, pick up from where we left off + $AppId = $Existing.AppId + $AppSecret = $null + + # Step 1: Create/update the app registration (idempotent) + # Pass stored AppId so we look up by clientId rather than name + $SSOAppParams = @{ + RedirectUri = $TargetUrl + MultiTenant = $MultiTenant + } + if ($AppId) { $SSOAppParams.ExistingAppId = $AppId } + + $SSOApp = New-CIPPSSOApp @SSOAppParams + $AppId = $SSOApp.AppId + $AppSecret = $SSOApp.ClientSecret + 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 = '' + } + 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') + } + 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" + } + + # 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 { } + + 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)" + } + + # 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" + } + + # 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 = '' + } + Add-CIPPAzDataTableEntity @MigrationTable -Entity $FinalRow -Force | Out-Null + + Write-LogMessage -API $APIName -headers $Headers -message "SSO migration credentials stored for app $AppId" -sev Info + $Body = @{ + Results = @{ + message = 'CIPP-SSO app created and credentials stored. EasyAuth will be configured automatically on next startup.' + appId = $AppId + multiTenant = $MultiTenant + severity = 'success' + } + } + } catch { + $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 { } + + $StatusCode = [HttpStatusCode]::InternalServerError + $Body = @{ Results = "SSO setup failed: $($ErrorMessage.NormalizedError)" } + } + } + + 'Update' { + # Update existing SSO app configuration (e.g. switch single ↔ multi-tenant) + try { + $Existing = Get-CIPPAzDataTableEntity @MigrationTable -Filter "PartitionKey eq 'SSO' and RowKey eq 'MigrationConfig'" -ErrorAction SilentlyContinue + # Fall back to live EasyAuth config if migration table has no entry + 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; Status = 'complete'; CreatedAt = $null } + } + } + if (-not $Existing -or -not $Existing.AppId) { + $StatusCode = [HttpStatusCode]::BadRequest + $Body = @{ Results = 'No SSO app has been created yet. Use the Create action first.' } + break + } + + $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)" + } + + $SignInAudience = if ($MultiTenant) { 'AzureADMultipleOrgs' } else { 'AzureADMyOrg' } + $CallbackUri = $TargetUrl.TrimEnd('/') + '/.auth/login/aad/callback' + + # Look up the existing app and patch it + $AppResponse = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/applications(appId='$($Existing.AppId)')?`$select=id,appId,web,signInAudience" -NoAuthCheck $true -AsApp $true + + $PatchBody = @{ + signInAudience = $SignInAudience + web = @{ + redirectUris = @($CallbackUri) + implicitGrantSettings = @{ enableIdTokenIssuance = $true } + } + } | ConvertTo-Json -Depth 10 -Compress + + 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 = '' + } + 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) + } + + # Update EasyAuth ARM config on the App Service (issuer URL + allowed tenants) + try { + Set-CIPPSSOEasyAuth -AppId $Existing.AppId -MultiTenant $MultiTenant -TenantId $env:TenantID + } catch { + Write-Information "[SSO-Update] EasyAuth ARM update skipped (may not be in App Service): $($_.Exception.Message)" + } + + $Body = @{ + Results = @{ + message = "SSO app updated successfully. Sign-in audience is now $SignInAudience." + appId = $Existing.AppId + multiTenant = $MultiTenant + severity = 'success' + } + } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API $APIName -headers $Headers -message "SSO update failed: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::InternalServerError + $Body = @{ Results = "SSO update failed: $($ErrorMessage.NormalizedError)" } + } + } + + 'RotateSecret' { + # Rotate the client secret for the SSO app + try { + $Existing = Get-CIPPAzDataTableEntity @MigrationTable -Filter "PartitionKey eq 'SSO' and RowKey eq 'MigrationConfig'" -ErrorAction SilentlyContinue + # Fall back to live EasyAuth config if migration table has no entry + 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 } + } + } + if (-not $Existing -or -not $Existing.AppId) { + $StatusCode = [HttpStatusCode]::BadRequest + $Body = @{ Results = 'No SSO app has been created yet.' } + break + } + + # 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' + } + + # 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) + } + + # Update last checked + $UpdateRow = @{ + PartitionKey = 'SSO' + RowKey = 'MigrationConfig' + LastChecked = (Get-Date).ToUniversalTime().ToString('o') + LastError = '' + } + Add-CIPPAzDataTableEntity @MigrationTable -Entity $UpdateRow -Force | Out-Null + + Write-LogMessage -API $APIName -headers $Headers -message "SSO app secret rotated for $($Existing.AppId)" -sev Info + $Body = @{ + Results = @{ + message = 'Client secret rotated successfully. The new secret will be picked up from Key Vault on next restart.' + severity = 'success' + } + } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API $APIName -headers $Headers -message "SSO secret rotation failed: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::InternalServerError + $Body = @{ Results = "Secret rotation failed: $($ErrorMessage.NormalizedError)" } + } + } + + 'Migrate' { + # Forced SSO migration. Creates the customer's own CIPP-SSO app, + # stores credentials in Key Vault, configures EasyAuth, and removes the migration + # trigger env var. The central migration app (implicit auth, no secret) is replaced + # by the customer's own app with a proper client secret. + if (-not $env:CIPP_SSO_MIGRATION_APPID) { + $Body = @{ Results = @{ message = 'No SSO migration pending.'; severity = 'info' } } + break + } + + $MultiTenant = [bool]($Request.Body.multiTenant) + $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 { } + } + + # Step 1: Create or update the customer's own CIPP-SSO app registration + $SSOAppParams = @{ + RedirectUri = $TargetUrl + MultiTenant = $MultiTenant + } + if ($ExistingAppId) { $SSOAppParams.ExistingAppId = $ExistingAppId } + + $SSOApp = New-CIPPSSOApp @SSOAppParams + $AppId = $SSOApp.AppId + $AppSecret = $SSOApp.ClientSecret + 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 3: Configure EasyAuth on the App Service + Set-CIPPSSOEasyAuth -AppId $AppId -MultiTenant $MultiTenant -TenantId $env:TenantID -UseKvReferences + + # Step 4: 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 + + 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') + + $Body = @{ + Results = @{ + message = 'SSO migration complete. Your instance will restart with your own CIPP-SSO app registration. You will be redirected to log in once the instance is back online.' + appId = $AppId + multiTenant = $MultiTenant + severity = 'success' + } + } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API $APIName -headers $Headers -message "SSO migration failed: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::InternalServerError + $Body = @{ Results = "SSO migration failed: $($ErrorMessage.NormalizedError)" } + } + } + + default { + $StatusCode = [HttpStatusCode]::BadRequest + $Body = @{ Results = "Unknown action: $Action. Use 'Status', 'Create', or 'Update'." } + } + } + + return [HttpResponseContext]@{ + StatusCode = $StatusCode ?? [HttpStatusCode]::OK + Body = $Body + } +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneTemplates.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneTemplates.ps1 index 2b0da1d4a696..4944d2f2f7cb 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneTemplates.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneTemplates.ps1 @@ -48,37 +48,94 @@ function Invoke-ListIntuneTemplates { } } | Sort-Object -Property displayName + + # Build a lookup of which standards templates reference each Intune template (by GUID or package) + $UsageByGuid = @{} + $UsageByPackage = @{} + $StdTemplates = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq 'StandardsTemplateV2'" + foreach ($StdRaw in $StdTemplates) { + try { + $StdData = $StdRaw.JSON | ConvertFrom-Json -Depth 100 -ErrorAction SilentlyContinue + if (-not $StdData -or -not $StdData.standards) { continue } + $IsDrift = $StdData.type -eq 'drift' + $StdInfo = [pscustomobject]@{ + templateName = $StdData.templateName + templateId = $StdRaw.RowKey + isDrift = $IsDrift + cippLink = "/tenant/standards/templates/template?id=$($StdRaw.RowKey)$(if ($IsDrift) { '&type=drift' })" + } + $IntuneEntries = $StdData.standards.IntuneTemplate + if (-not $IntuneEntries) { continue } + $Items = if ($IntuneEntries -is [System.Collections.IEnumerable] -and $IntuneEntries -isnot [string]) { $IntuneEntries } else { @($IntuneEntries) } + foreach ($Item in $Items) { + if ($Item.TemplateList.value) { + $Guid = $Item.TemplateList.value + if (-not $UsageByGuid.ContainsKey($Guid)) { $UsageByGuid[$Guid] = [System.Collections.Generic.List[object]]::new() } + $UsageByGuid[$Guid].Add($StdInfo) + } + if ($Item.'TemplateList-Tags'.value) { + $Pkg = $Item.'TemplateList-Tags'.value + if (-not $UsageByPackage.ContainsKey($Pkg)) { $UsageByPackage[$Pkg] = [System.Collections.Generic.List[object]]::new() } + $UsageByPackage[$Pkg].Add($StdInfo) + } + } + } catch {} + } + + # Attach usage list to each Intune template + foreach ($Tpl in $Templates) { + $Usage = [System.Collections.Generic.List[object]]::new() + if ($Tpl.GUID -and $UsageByGuid.ContainsKey($Tpl.GUID)) { + foreach ($U in $UsageByGuid[$Tpl.GUID]) { + $Entry = $U | Select-Object * + $Entry | Add-Member -NotePropertyName 'matchType' -NotePropertyValue 'direct' -Force + $Usage.Add($Entry) + } + } + if ($Tpl.package -and $UsageByPackage.ContainsKey($Tpl.package)) { + foreach ($U in $UsageByPackage[$Tpl.package]) { + # Avoid duplicates when the same template matches both GUID and package + if (-not ($Usage | Where-Object { $_.templateId -eq $U.templateId })) { + $Entry = $U | Select-Object * + $Entry | Add-Member -NotePropertyName 'matchType' -NotePropertyValue 'package' -Force + $Entry | Add-Member -NotePropertyName 'package' -NotePropertyValue $Tpl.package -Force + $Usage.Add($Entry) + } + } + } + $Tpl | Add-Member -NotePropertyName 'usage' -NotePropertyValue @($Usage) -Force + } } else { if ($Request.query.mode -eq 'Tag') { #when the mode is tag, show all the potential tags, return the object with: label: tag, value: tag, count: number of templates with that tag, unique only $Templates = @($RawTemplates | Where-Object { $_.Package } | Group-Object -Property Package | ForEach-Object { - $package = $_.Name - $packageTemplates = @($_.Group) - $templateCount = $packageTemplates.Count - [pscustomobject]@{ - label = "$($package) ($templateCount Templates)" - value = $package - type = 'tag' - templateCount = $templateCount - templates = @($packageTemplates | ForEach-Object { - try { - $JSONData = $_.JSON | ConvertFrom-Json -Depth 100 -ErrorAction SilentlyContinue - $data = $JSONData.RAWJson | ConvertFrom-Json -Depth 100 -ErrorAction SilentlyContinue - $data | Add-Member -NotePropertyName 'displayName' -NotePropertyValue $JSONData.Displayname -Force - $data | Add-Member -NotePropertyName 'description' -NotePropertyValue $JSONData.Description -Force - $data | Add-Member -NotePropertyName 'Type' -NotePropertyValue $JSONData.Type -Force - $data | Add-Member -NotePropertyName 'GUID' -NotePropertyValue $_.RowKey -Force - $data | Add-Member -NotePropertyName 'package' -NotePropertyValue $_.Package -Force - $data | Add-Member -NotePropertyName 'source' -NotePropertyValue $_.Source -Force - $data | Add-Member -NotePropertyName 'isSynced' -NotePropertyValue (![string]::IsNullOrEmpty($_.SHA)) -Force - $data | Add-Member -NotePropertyName 'reusableSettings' -NotePropertyValue $JSONData.ReusableSettings -Force - $data - } catch { + $package = $_.Name + $packageTemplates = @($_.Group) + $templateCount = $packageTemplates.Count + [pscustomobject]@{ + label = "$($package) ($templateCount Templates)" + value = $package + type = 'tag' + templateCount = $templateCount + templates = @($packageTemplates | ForEach-Object { + try { + $JSONData = $_.JSON | ConvertFrom-Json -Depth 100 -ErrorAction SilentlyContinue + $data = $JSONData.RAWJson | ConvertFrom-Json -Depth 100 -ErrorAction SilentlyContinue + $data | Add-Member -NotePropertyName 'displayName' -NotePropertyValue $JSONData.Displayname -Force + $data | Add-Member -NotePropertyName 'description' -NotePropertyValue $JSONData.Description -Force + $data | Add-Member -NotePropertyName 'Type' -NotePropertyValue $JSONData.Type -Force + $data | Add-Member -NotePropertyName 'GUID' -NotePropertyValue $_.RowKey -Force + $data | Add-Member -NotePropertyName 'package' -NotePropertyValue $_.Package -Force + $data | Add-Member -NotePropertyName 'source' -NotePropertyValue $_.Source -Force + $data | Add-Member -NotePropertyName 'isSynced' -NotePropertyValue (![string]::IsNullOrEmpty($_.SHA)) -Force + $data | Add-Member -NotePropertyName 'reusableSettings' -NotePropertyValue $JSONData.ReusableSettings -Force + $data + } catch { - } - }) - } - } | Sort-Object -Property label) + } + }) + } + } | Sort-Object -Property label) } else { $Templates = $RawTemplates.JSON | ForEach-Object { try { ConvertFrom-Json -InputObject $_ -Depth 100 -ErrorAction SilentlyContinue } catch {} } diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBECRemediate.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBECRemediate.ps1 index 29bf889d3773..f9516ce5ab29 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBECRemediate.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBECRemediate.ps1 @@ -180,6 +180,21 @@ function Invoke-ExecBECRemediate { }) } + # Step 6: Disable OneDrive Sharing + $Step = 'Disable OneDrive Sharing' + try { + $OneDriveResult = Set-CIPPOneDriveSharing -UserId $Username -TenantFilter $TenantFilter -SharingCapability 'Disabled' -APIName $APIName -Headers $Headers + $AllResults.Add([pscustomobject]@{ + resultText = $OneDriveResult + state = if ($OneDriveResult -like '*Successfully*') { 'success' } else { 'error' } + }) + } catch { + $AllResults.Add([pscustomobject]@{ + resultText = "Failed to disable OneDrive sharing: $($_.Exception.Message)" + state = 'error' + }) + } + $StatusCode = [HttpStatusCode]::OK Write-LogMessage -API 'BECRemediate' -tenant $TenantFilter -message "Executed Remediation for $Username" -sev 'Info' -LogData @($AllResults) 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 = @{ diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ExecUniversalSearchV2.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ExecUniversalSearchV2.ps1 index 6bcb3befbb49..8c3b9d954fb1 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ExecUniversalSearchV2.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ExecUniversalSearchV2.ps1 @@ -28,6 +28,9 @@ function Invoke-ExecUniversalSearchV2 { 'Groups' { $Results = Search-CIPPDbData -SearchTerms $SearchTerms -Types 'Groups' -Limit $Limit -Properties 'id', 'displayName', 'mail', 'mailEnabled', 'securityEnabled', 'groupTypes', 'description' -TenantFilter $TenantFilter } + 'Applications' { + $Results = Search-CIPPDbData -SearchTerms $SearchTerms -Types 'Apps', 'ServicePrincipals' -Limit $Limit -Properties 'id', 'appId', 'displayName', 'publisherName', 'appOwnerOrganizationId' -TenantFilter $TenantFilter + } default { $Results = Search-CIPPDbData -SearchTerms $SearchTerms -Types 'Users' -Limit $Limit -Properties 'id', 'userPrincipalName', 'displayName' -TenantFilter $TenantFilter } diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ExecSetOneDriveSharing.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ExecSetOneDriveSharing.ps1 new file mode 100644 index 000000000000..3bffa6f834d0 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ExecSetOneDriveSharing.ps1 @@ -0,0 +1,37 @@ +function Invoke-ExecSetOneDriveSharing { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Teams.SharePoint.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $TenantFilter = $Request.Body.tenantFilter + $UserPrincipalName = $Request.Body.UPN + $SharingCapability = $Request.Body.SharingCapability.value ?? $Request.Body.SharingCapability + + try { + $Result = Set-CIPPOneDriveSharing ` + -UserId $UserPrincipalName ` + -TenantFilter $TenantFilter ` + -SharingCapability $SharingCapability ` + -APIName $APIName ` + -Headers $Request.Headers + + $Body = @{ Results = $Result } + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API $APIName -tenant $TenantFilter -message "Failed to set OneDrive sharing: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + $Body = @{ Results = "Failed to set OneDrive sharing: $($ErrorMessage.NormalizedError)" } + $StatusCode = [HttpStatusCode]::InternalServerError + } + + return [HttpResponseContext]@{ + StatusCode = $StatusCode + Body = $Body + } +} 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 d8aae26f5998..0335d3357557 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 @@ -78,9 +78,17 @@ function Invoke-ListSites { $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)) - $Usage = ($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)) + $Usage = ($UsageJson | ConvertFrom-Json).value + } else { + $Usage = @($UsageBody.value) + } $GraphRequest = foreach ($Site in $Sites) { $SiteUsage = $Usage | Where-Object { $_.siteId -eq $Site.sharepointIds.siteId } diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ExecCreateCATemplate.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ExecCreateCATemplate.ps1 new file mode 100644 index 000000000000..28d932d2d8aa --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ExecCreateCATemplate.ps1 @@ -0,0 +1,54 @@ +function Invoke-ExecCreateCATemplate { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + Tenant.ConditionalAccess.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + try { + $Body = $Request.Body + $DisplayName = $Body.displayName ?? $Body.name + if ([string]::IsNullOrWhiteSpace($DisplayName)) { + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = @{ Results = 'Error: displayName is required' } + } + } + + $GUID = (New-Guid).GUID + + # Strip any read-only or internal properties before storing + $Template = $Body | Select-Object -Property * -ExcludeProperty GUID, id, createdDateTime, modifiedDateTime, templateId + + $JSON = ConvertTo-Json -InputObject $Template -Depth 100 -Compress + + $Table = Get-CippTable -tablename 'templates' + $Table.Force = $true + Add-CIPPAzDataTableEntity @Table -Entity @{ + JSON = "$JSON" + RowKey = "$GUID" + PartitionKey = 'CATemplate' + GUID = "$GUID" + } + + $Result = "Successfully created CA template '$DisplayName' with GUID $GUID" + Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Info' + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to create CA template: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Error' -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::InternalServerError + } + + return [HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{ Results = $Result } + } +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ExecEditCAPolicyFull.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ExecEditCAPolicyFull.ps1 new file mode 100644 index 000000000000..40c347946fe2 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ExecEditCAPolicyFull.ps1 @@ -0,0 +1,64 @@ +function Invoke-ExecEditCAPolicyFull { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Tenant.ConditionalAccess.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter + + if ([string]::IsNullOrWhiteSpace($TenantFilter)) { + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = @{ Results = 'Error: tenantFilter is required' } + } + } + + $PolicyId = $Request.Query.PolicyId ?? $Request.Body.PolicyId + if ([string]::IsNullOrWhiteSpace($PolicyId)) { + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = @{ Results = 'Error: PolicyId is required' } + } + } + + try { + $PolicyBody = $Request.Body.PolicyBody + if ($null -eq $PolicyBody) { + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = @{ Results = 'Error: PolicyBody is required' } + } + } + + # Strip read-only properties that cannot be PATCHed + $CleanBody = $PolicyBody | Select-Object -Property * -ExcludeProperty id, createdDateTime, modifiedDateTime, templateId + $RawJSON = ConvertTo-Json -InputObject $CleanBody -Depth 20 -Compress + + $null = New-GraphPOSTRequest ` + -uri "https://graph.microsoft.com/beta/identity/conditionalAccess/policies/$PolicyId" ` + -tenantid $TenantFilter ` + -type PATCH ` + -body $RawJSON ` + -asApp $true + + $DisplayName = $PolicyBody.displayName ?? $PolicyId + $Result = "Successfully updated CA policy '$DisplayName' for $TenantFilter" + Write-LogMessage -API $APIName -tenant $TenantFilter -headers $Headers -message $Result -sev Info + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to update CA policy $PolicyId for ${TenantFilter}: $($ErrorMessage.NormalizedError)" + Write-LogMessage -API $APIName -tenant $TenantFilter -headers $Headers -message $Result -sev Error -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::InternalServerError + } + + return [HttpResponseContext]@{ + StatusCode = $StatusCode ?? [HttpStatusCode]::OK + Body = @{ Results = $Result } + } +} diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAddDKIM.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAddDKIM.ps1 index a802210209ea..647323917e74 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAddDKIM.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAddDKIM.ps1 @@ -42,7 +42,7 @@ function Invoke-CIPPStandardAddDKIM { param($Tenant, $Settings) #$Rerun -Type Standard -Tenant $Tenant -API 'AddDKIM' -Settings $Settings - $TestResult = Test-CIPPStandardLicense -StandardName 'AddDKIM' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'AddDKIM' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAntiPhishPolicy.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAntiPhishPolicy.ps1 index 4280118eb153..855f555caf19 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAntiPhishPolicy.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAntiPhishPolicy.ps1 @@ -81,7 +81,7 @@ function Invoke-CIPPStandardAntiPhishPolicy { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'AntiPhishPolicy' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'AntiPhishPolicy' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAntiSpamSafeList.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAntiSpamSafeList.ps1 index 48382dd29d15..e6de258ac65a 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAntiSpamSafeList.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAntiSpamSafeList.ps1 @@ -38,7 +38,7 @@ function Invoke-CIPPStandardAntiSpamSafeList { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'AntiSpamSafeList' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'AntiSpamSafeList' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAssignmentFilterTemplate.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAssignmentFilterTemplate.ps1 index f868752f4f53..4e63aaaa7d95 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAssignmentFilterTemplate.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAssignmentFilterTemplate.ps1 @@ -37,7 +37,7 @@ function Invoke-CIPPStandardAssignmentFilterTemplate { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'AssignmentFilterTemplate' -TenantFilter $Tenant -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') + $TestResult = Test-CIPPStandardLicense -StandardName 'AssignmentFilterTemplate' -TenantFilter $Tenant -Preset Intune if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAtpPolicyForO365.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAtpPolicyForO365.ps1 index 94a87652da86..e89461cf1430 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAtpPolicyForO365.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAtpPolicyForO365.ps1 @@ -40,14 +40,14 @@ function Invoke-CIPPStandardAtpPolicyForO365 { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'AtpPolicyForO365' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'SHAREPOINTENTERPRISE_GOV', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') + $TestResult = Test-CIPPStandardLicense -StandardName 'AtpPolicyForO365' -TenantFilter $Tenant -Preset SharePoint ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'AtpPolicyForO365' if ($TestResult -eq $false) { return $true } #we're done. - $MDOTestResult = Test-CIPPStandardLicense -StandardName 'AtpPolicyForO365' -TenantFilter $Tenant -RequiredCapabilities @('ATP_ENTERPRISE', 'ATP_ENTERPRISE_GOV', 'THREAT_INTELLIGENCE') + $MDOTestResult = Test-CIPPStandardLicense -StandardName 'AtpPolicyForO365' -TenantFilter $Tenant -Preset DefenderForOffice365 if ($MDOTestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAuditLog.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAuditLog.ps1 index f36021febb7b..81e089878460 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAuditLog.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAuditLog.ps1 @@ -43,7 +43,7 @@ function Invoke-CIPPStandardAuditLog { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'AuditLog' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'AuditLog' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAutoAddProxy.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAutoAddProxy.ps1 index efb8efdee4a3..6acdb097014f 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAutoAddProxy.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAutoAddProxy.ps1 @@ -36,7 +36,7 @@ function Invoke-CIPPStandardAutoAddProxy { $QueueItem ) - $TestResult = Test-CIPPStandardLicense -StandardName 'AutoArchive' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') + $TestResult = Test-CIPPStandardLicense -StandardName 'AutoArchive' -TenantFilter $Tenant -Preset Exchange if ($TestResult -eq $false) { return $true } @@ -66,13 +66,13 @@ function Invoke-CIPPStandardAutoAddProxy { $MissingProxies = 0 foreach ($Domain in $Domains) { $ProcessMailboxes = @($AllMailboxes | Where-Object { - $AllAddresses = @($_.primarySmtpAddress) - if (-not [string]::IsNullOrWhiteSpace($_.AdditionalEmailAddresses)) { - $AllAddresses += @($_.AdditionalEmailAddresses -split ',\s*') - } - $HasDomain = $AllAddresses | Where-Object { $_ -like "*@$Domain" } - -not $HasDomain - }) + $AllAddresses = @($_.primarySmtpAddress) + if (-not [string]::IsNullOrWhiteSpace($_.AdditionalEmailAddresses)) { + $AllAddresses += @($_.AdditionalEmailAddresses -split ',\s*') + } + $HasDomain = $AllAddresses | Where-Object { $_ -like "*@$Domain" } + -not $HasDomain + }) $MissingProxies += $ProcessMailboxes.Count } @@ -104,13 +104,13 @@ function Invoke-CIPPStandardAutoAddProxy { } else { foreach ($Domain in $Domains) { $ProcessMailboxes = @($AllMailboxes | Where-Object { - $AllAddresses = @($_.primarySmtpAddress) - if (-not [string]::IsNullOrWhiteSpace($_.AdditionalEmailAddresses)) { - $AllAddresses += @($_.AdditionalEmailAddresses -split ',\s*') - } - $HasDomain = $AllAddresses | Where-Object { $_ -like "*@$Domain" } - -not $HasDomain - }) + $AllAddresses = @($_.primarySmtpAddress) + if (-not [string]::IsNullOrWhiteSpace($_.AdditionalEmailAddresses)) { + $AllAddresses += @($_.AdditionalEmailAddresses -split ',\s*') + } + $HasDomain = $AllAddresses | Where-Object { $_ -like "*@$Domain" } + -not $HasDomain + }) $bulkRequest = foreach ($Mailbox in $ProcessMailboxes) { if ([string]::IsNullOrWhiteSpace($Mailbox.UPN)) { continue } diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAutoArchive.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAutoArchive.ps1 index 703363e49f57..55830e0b5179 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAutoArchive.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAutoArchive.ps1 @@ -37,7 +37,7 @@ function Invoke-CIPPStandardAutoArchive { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'AutoArchive' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') + $TestResult = Test-CIPPStandardLicense -StandardName 'AutoArchive' -TenantFilter $Tenant -Preset Exchange if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAutoArchiveMailbox.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAutoArchiveMailbox.ps1 index 5953686c5793..662d28b3d0b8 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAutoArchiveMailbox.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAutoArchiveMailbox.ps1 @@ -37,7 +37,7 @@ function Invoke-CIPPStandardAutoArchiveMailbox { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'AutoArchiveMailbox' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') + $TestResult = Test-CIPPStandardLicense -StandardName 'AutoArchiveMailbox' -TenantFilter $Tenant -Preset Exchange if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAutoExpandArchive.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAutoExpandArchive.ps1 index dc59e3dc4bac..111b785dc0da 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAutoExpandArchive.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAutoExpandArchive.ps1 @@ -36,7 +36,7 @@ function Invoke-CIPPStandardAutoExpandArchive { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'AutoExpandArchive' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'AutoExpandArchive' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAutopilotProfile.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAutopilotProfile.ps1 index 4c70f0a60951..f8f1e723eef5 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAutopilotProfile.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAutopilotProfile.ps1 @@ -46,7 +46,7 @@ function Invoke-CIPPStandardAutopilotProfile { https://docs.cipp.app/user-documentation/tenant/standards/alignment/templates/available-standards #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'AutopilotProfile' -TenantFilter $Tenant -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') + $TestResult = Test-CIPPStandardLicense -StandardName 'AutopilotProfile' -TenantFilter $Tenant -Preset Intune # Get the current configuration diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAutopilotStatusPage.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAutopilotStatusPage.ps1 index d141d5a8f3c2..f6e6e73dd123 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAutopilotStatusPage.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardAutopilotStatusPage.ps1 @@ -44,7 +44,7 @@ function Invoke-CIPPStandardAutopilotStatusPage { https://docs.cipp.app/user-documentation/tenant/standards/alignment/templates/available-standards #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'AutopilotStatusPage' -TenantFilter $Tenant -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') + $TestResult = Test-CIPPStandardLicense -StandardName 'AutopilotStatusPage' -TenantFilter $Tenant -Preset Intune # Get current Autopilot enrollment status page configuration diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardBookings.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardBookings.ps1 index 16b2ae4abd0d..9173dd91b51d 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardBookings.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardBookings.ps1 @@ -37,7 +37,7 @@ function Invoke-CIPPStandardBookings { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'Bookings' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'Bookings' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardBranding.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardBranding.ps1 index 2a3be6f4fe6b..fc1c34e5be27 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardBranding.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardBranding.ps1 @@ -41,7 +41,7 @@ function Invoke-CIPPStandardBranding { param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'Branding' -TenantFilter $Tenant -RequiredCapabilities @('AAD_PREMIUM', 'AAD_PREMIUM_P2', 'OFFICE_BUSINESS') + $TestResult = Test-CIPPStandardLicense -StandardName 'Branding' -TenantFilter $Tenant -Preset Entra -RequiredCapabilities @('OFFICE_BUSINESS') if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardCloudMessageRecall.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardCloudMessageRecall.ps1 index 5c45a2da45a5..0d2b67f55a5a 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardCloudMessageRecall.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardCloudMessageRecall.ps1 @@ -37,7 +37,7 @@ function Invoke-CIPPStandardCloudMessageRecall { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'CloudMessageRecall' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'CloudMessageRecall' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardColleagueImpersonationAlert.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardColleagueImpersonationAlert.ps1 index da1bfd5178e5..34ca096ac19a 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardColleagueImpersonationAlert.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardColleagueImpersonationAlert.ps1 @@ -45,7 +45,7 @@ function Invoke-CIPPStandardColleagueImpersonationAlert { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'ColleagueImpersonationAlert' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') + $TestResult = Test-CIPPStandardLicense -StandardName 'ColleagueImpersonationAlert' -TenantFilter $Tenant -Preset Exchange if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 index f62298b0a57d..4d5c8447da02 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 @@ -48,7 +48,7 @@ function Invoke-CIPPStandardConditionalAccessTemplate { } - $TestResult = Test-CIPPStandardLicense -StandardName 'ConditionalAccessTemplate_general' -TenantFilter $Tenant -RequiredCapabilities @('AAD_PREMIUM', 'AAD_PREMIUM_P2') + $TestResult = Test-CIPPStandardLicense -StandardName 'ConditionalAccessTemplate_general' -TenantFilter $Tenant -Preset Entra if ($TestResult -eq $false) { Set-CIPPStandardsCompareField -FieldName "standards.ConditionalAccessTemplate.$($Settings.TemplateList.value)" -FieldValue 'This tenant does not have the required license for this standard.' -Tenant $Tenant return $true @@ -72,7 +72,7 @@ function Invoke-CIPPStandardConditionalAccessTemplate { $JSONObj = (Get-CippAzDataTableEntity @Table -Filter $Filter).JSON $Policy = $JSONObj | ConvertFrom-Json if ($Policy.conditions.userRiskLevels.count -gt 0 -or $Policy.conditions.signInRiskLevels.count -gt 0) { - $TestP2 = Test-CIPPStandardLicense -StandardName 'ConditionalAccessTemplate_p2' -TenantFilter $Tenant -RequiredCapabilities @('AAD_PREMIUM_P2') -SkipLog + $TestP2 = Test-CIPPStandardLicense -StandardName 'ConditionalAccessTemplate_p2' -TenantFilter $Tenant -Preset EntraP2 -SkipLog if (!$TestP2) { Write-Information "Skipping policy $($Policy.displayName) as it requires AAD Premium P2 license." Set-CIPPStandardsCompareField -FieldName "standards.ConditionalAccessTemplate.$($Settings.TemplateList.value)" -FieldValue "Policy $($Policy.displayName) requires AAD Premium P2 license." -Tenant $Tenant @@ -113,7 +113,7 @@ function Invoke-CIPPStandardConditionalAccessTemplate { $CheckExististing = $AllCAPolicies | Where-Object -Property displayName -EQ $Settings.TemplateList.label if (!$CheckExististing) { if ($Policy.conditions.userRiskLevels.Count -gt 0 -or $Policy.conditions.signInRiskLevels.Count -gt 0) { - $TestP2 = Test-CIPPStandardLicense -StandardName 'ConditionalAccessTemplate_p2' -TenantFilter $Tenant -RequiredCapabilities @('AAD_PREMIUM_P2') -SkipLog + $TestP2 = Test-CIPPStandardLicense -StandardName 'ConditionalAccessTemplate_p2' -TenantFilter $Tenant -Preset EntraP2 -SkipLog if (!$TestP2) { Set-CIPPStandardsCompareField -FieldName "standards.ConditionalAccessTemplate.$($Settings.TemplateList.value)" -FieldValue "Policy $($Settings.TemplateList.label) requires AAD Premium P2 license." -Tenant $Tenant } else { diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardCustomBannedPasswordList.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardCustomBannedPasswordList.ps1 index 7ccc99824d66..427bff282481 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardCustomBannedPasswordList.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardCustomBannedPasswordList.ps1 @@ -43,7 +43,7 @@ function Invoke-CIPPStandardCustomBannedPasswordList { param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'CustomBannedPasswordList' -TenantFilter $Tenant -RequiredCapabilities @('AAD_PREMIUM', 'AAD_PREMIUM_P2') + $TestResult = Test-CIPPStandardLicense -StandardName 'CustomBannedPasswordList' -TenantFilter $Tenant -Preset Entra if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDefaultPlatformRestrictions.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDefaultPlatformRestrictions.ps1 index e1bfe0fd8f14..1299880d7ed3 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDefaultPlatformRestrictions.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDefaultPlatformRestrictions.ps1 @@ -47,7 +47,7 @@ function Invoke-CIPPStandardDefaultPlatformRestrictions { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'DefaultPlatformRestrictions' -TenantFilter $Tenant -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') + $TestResult = Test-CIPPStandardLicense -StandardName 'DefaultPlatformRestrictions' -TenantFilter $Tenant -Preset Intune if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDefaultSharingLink.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDefaultSharingLink.ps1 index 66d6b1d5e393..7a82cb1e947a 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDefaultSharingLink.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDefaultSharingLink.ps1 @@ -46,7 +46,7 @@ function Invoke-CIPPStandardDefaultSharingLink { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'DefaultSharingLink' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'SHAREPOINTENTERPRISE_GOV', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') + $TestResult = Test-CIPPStandardLicense -StandardName 'DefaultSharingLink' -TenantFilter $Tenant -Preset SharePoint # Determine the desired sharing link type (default to Internal if not specified) diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDelegateSentItems.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDelegateSentItems.ps1 index b7a757b2e5a2..35b7259a707e 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDelegateSentItems.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDelegateSentItems.ps1 @@ -37,7 +37,7 @@ function Invoke-CIPPStandardDelegateSentItems { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'DelegateSentItems' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'DelegateSentItems' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDeletedUserRentention.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDeletedUserRentention.ps1 index c54beb8ba6bc..bf21fb3ff637 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDeletedUserRentention.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDeletedUserRentention.ps1 @@ -39,7 +39,7 @@ function Invoke-CIPPStandardDeletedUserRentention { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'DeletedUserRentention' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'SHAREPOINTENTERPRISE_GOV', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') + $TestResult = Test-CIPPStandardLicense -StandardName 'DeletedUserRentention' -TenantFilter $Tenant -Preset SharePoint ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'DeletedUserRetention' if ($TestResult -eq $false) { diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDeployCheckChromeExtension.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDeployCheckChromeExtension.ps1 index 413c5b6f9594..01469ff25668 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDeployCheckChromeExtension.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDeployCheckChromeExtension.ps1 @@ -64,7 +64,7 @@ function Invoke-CIPPStandardDeployCheckChromeExtension { param($Tenant, $Settings) # Check for required Intune license - $TestResult = Test-CIPPStandardLicense -StandardName 'DeployCheckChromeExtension' -TenantFilter $Tenant -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') + $TestResult = Test-CIPPStandardLicense -StandardName 'DeployCheckChromeExtension' -TenantFilter $Tenant -Preset Intune if ($TestResult -eq $false) { Set-CIPPStandardsCompareField -FieldName 'standards.DeployCheckChromeExtension' -FieldValue 'This tenant does not have the required license for this standard.' -Tenant $Tenant @@ -137,7 +137,7 @@ function Invoke-CIPPStandardDeployCheckChromeExtension { ExtSettingsKey = "HKLM:\SOFTWARE\Policies\Microsoft\Edge\ExtensionSettings\`$edgeExtensionId" ToolbarProp = 'toolbar_state' ToolbarPinned = 'force_shown' - ToolbarUnpinned = 'hidden' + ToolbarUnpinned = 'default_hidden' } ) diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 index f346b1487141..3290eeacc133 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 @@ -40,7 +40,7 @@ function Invoke-CIPPStandardDeployContactTemplates { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'DeployContactTemplates' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'DeployContactTemplates' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1 index 8dc54cd98995..a6ddad102a4d 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1 @@ -41,7 +41,7 @@ function Invoke-CIPPStandardDeployMailContact { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'DeployMailContact' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'DeployMailContact' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableAddShortcutsToOneDrive.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableAddShortcutsToOneDrive.ps1 index c8db2e7c65a7..45e9517e989b 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableAddShortcutsToOneDrive.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableAddShortcutsToOneDrive.ps1 @@ -39,7 +39,7 @@ function Invoke-CIPPStandardDisableAddShortcutsToOneDrive { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'DisableAddShortcutsToOneDrive' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'SHAREPOINTENTERPRISE_GOV', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') + $TestResult = Test-CIPPStandardLicense -StandardName 'DisableAddShortcutsToOneDrive' -TenantFilter $Tenant -Preset SharePoint ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'DisableAddShortcutsToOneDrive' if ($TestResult -eq $false) { diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableAdditionalStorageProviders.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableAdditionalStorageProviders.ps1 index 897e873c457a..1bb71fa818d1 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableAdditionalStorageProviders.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableAdditionalStorageProviders.ps1 @@ -40,7 +40,7 @@ function Invoke-CIPPStandardDisableAdditionalStorageProviders { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'DisableAdditionalStorageProviders' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'DisableAdditionalStorageProviders' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 index bee985a717b8..64c39c224c69 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 @@ -42,7 +42,7 @@ function Invoke-CIPPStandardDisableBasicAuthSMTP { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'DisableBasicAuthSMTP' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'DisableBasicAuthSMTP' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableEWS.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableEWS.ps1 index a0bbb6de3d3c..27d90969379c 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableEWS.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableEWS.ps1 @@ -37,7 +37,7 @@ function Invoke-CIPPStandardDisableEWS { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'DisableEWS' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') + $TestResult = Test-CIPPStandardLicense -StandardName 'DisableEWS' -TenantFilter $Tenant -Preset Exchange if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableExchangeOnlinePowerShell.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableExchangeOnlinePowerShell.ps1 index f069cb8a5b66..c801a9924e67 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableExchangeOnlinePowerShell.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableExchangeOnlinePowerShell.ps1 @@ -40,7 +40,7 @@ function Invoke-CIPPStandardDisableExchangeOnlinePowerShell { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'DisableExchangeOnlinePowerShell' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'DisableExchangeOnlinePowerShell' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableExternalCalendarSharing.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableExternalCalendarSharing.ps1 index a238e9444f9b..dc3ecc0b6e28 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableExternalCalendarSharing.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableExternalCalendarSharing.ps1 @@ -41,7 +41,7 @@ function Invoke-CIPPStandardDisableExternalCalendarSharing { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'DisableExternalCalendarSharing' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'DisableExternalCalendarSharing' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableGuests.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableGuests.ps1 index c5388256bd53..aaecff145875 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableGuests.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableGuests.ps1 @@ -37,7 +37,7 @@ function Invoke-CIPPStandardDisableGuests { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'DisableGuests' -TenantFilter $Tenant -RequiredCapabilities @('AAD_PREMIUM', 'AAD_PREMIUM_P2') + $TestResult = Test-CIPPStandardLicense -StandardName 'DisableGuests' -TenantFilter $Tenant -Preset Entra if ($TestResult -eq $false) { #writing to each item that the license is not present. diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableM365GroupUsers.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableM365GroupUsers.ps1 index 1de0b8efe605..43f21d705d6b 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableM365GroupUsers.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableM365GroupUsers.ps1 @@ -40,7 +40,7 @@ function Invoke-CIPPStandardDisableM365GroupUsers { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'DisableM365GroupUsers' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'SHAREPOINTENTERPRISE_GOV', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') + $TestResult = Test-CIPPStandardLicense -StandardName 'DisableM365GroupUsers' -TenantFilter $Tenant -Preset SharePoint if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableOutlookAddins.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableOutlookAddins.ps1 index cb9dcaac9a24..6038fd170770 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableOutlookAddins.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableOutlookAddins.ps1 @@ -42,7 +42,7 @@ function Invoke-CIPPStandardDisableOutlookAddins { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'DisableOutlookAddins' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'DisableOutlookAddins' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableReshare.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableReshare.ps1 index d526a2799051..3822b3087fa9 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableReshare.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableReshare.ps1 @@ -45,7 +45,7 @@ function Invoke-CIPPStandardDisableReshare { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'DisableReshare' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'SHAREPOINTENTERPRISE_GOV', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') + $TestResult = Test-CIPPStandardLicense -StandardName 'DisableReshare' -TenantFilter $Tenant -Preset SharePoint if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 index 1935547c5908..041c7e215e25 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 @@ -39,7 +39,7 @@ function Invoke-CIPPStandardDisableResourceMailbox { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'DisableResourceMailbox' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'DisableResourceMailbox' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableSharePointLegacyAuth.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableSharePointLegacyAuth.ps1 index ffe16e224402..2a14009825a0 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableSharePointLegacyAuth.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableSharePointLegacyAuth.ps1 @@ -47,7 +47,7 @@ function Invoke-CIPPStandardDisableSharePointLegacyAuth { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'DisableSharePointLegacyAuth' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'SHAREPOINTENTERPRISE_GOV', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') + $TestResult = Test-CIPPStandardLicense -StandardName 'DisableSharePointLegacyAuth' -TenantFilter $Tenant -Preset SharePoint if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableTNEF.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableTNEF.ps1 index 8777d4739cc3..66b8ef6c8cac 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableTNEF.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableTNEF.ps1 @@ -37,7 +37,7 @@ function Invoke-CIPPStandardDisableTNEF { #> param ($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'DisableTNEF' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'DisableTNEF' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableUserSiteCreate.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableUserSiteCreate.ps1 index 70c46d6ccfcc..979bef30ca9e 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableUserSiteCreate.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableUserSiteCreate.ps1 @@ -38,7 +38,7 @@ function Invoke-CIPPStandardDisableUserSiteCreate { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'DisableUserSiteCreate' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'SHAREPOINTENTERPRISE_GOV', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') + $TestResult = Test-CIPPStandardLicense -StandardName 'DisableUserSiteCreate' -TenantFilter $Tenant -Preset SharePoint if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableViva.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableViva.ps1 index 542d8edc54cb..6cf276f3ce92 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableViva.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableViva.ps1 @@ -46,7 +46,7 @@ function Invoke-CIPPStandardDisableViva { } else { try { # TODO This does not work without Global Admin permissions for some reason. Throws an "EXCEPTION: Tenant admin role is required" error. -Bobby - New-GraphPOSTRequest -Uri "https://graph.microsoft.com/beta/organization/$Tenant/settings/peopleInsights" -tenantid $Tenant -Type PATCH -Body '{"isEnabledInOrganization": false}' -ContentType 'application/json' + New-GraphPOSTRequest -Uri "https://graph.microsoft.com/beta/organization/$Tenant/settings/peopleInsights" -tenantid $Tenant -Type PATCH -Body '{"isEnabledInOrganization": false}' -ContentType 'application/json' -AsApp $true Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Disabled Viva insights' -sev Info } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEXODisableAutoForwarding.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEXODisableAutoForwarding.ps1 index 95d5cd6c97c5..17e24bef9696 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEXODisableAutoForwarding.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEXODisableAutoForwarding.ps1 @@ -43,7 +43,7 @@ function Invoke-CIPPStandardEXODisableAutoForwarding { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'EXODisableAutoForwarding' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'EXODisableAutoForwarding' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEXOOutboundSpamLimits.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEXOOutboundSpamLimits.ps1 index 5be60254dc55..aa9380da7d0f 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEXOOutboundSpamLimits.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEXOOutboundSpamLimits.ps1 @@ -43,7 +43,7 @@ function Invoke-CIPPStandardEXOOutboundSpamLimits { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'EXOOutboundSpamLimits' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'EXOOutboundSpamLimits' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEmptyFilterIPAllowList.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEmptyFilterIPAllowList.ps1 index 834a84e735ec..f8bb29881803 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEmptyFilterIPAllowList.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEmptyFilterIPAllowList.ps1 @@ -39,7 +39,7 @@ function Invoke-CIPPStandardEmptyFilterIPAllowList { param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'EmptyFilterIPAllowList' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') + $TestResult = Test-CIPPStandardLicense -StandardName 'EmptyFilterIPAllowList' -TenantFilter $Tenant -Preset Exchange if ($TestResult -eq $false) { return $true } try { diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEnableExchangeCloudManagement.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEnableExchangeCloudManagement.ps1 index 157fab38e93b..da1af84b41ac 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEnableExchangeCloudManagement.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEnableExchangeCloudManagement.ps1 @@ -39,7 +39,7 @@ function Invoke-CIPPStandardEnableExchangeCloudManagement { param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'EnableExchangeCloudManagement' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV') + $TestResult = Test-CIPPStandardLicense -StandardName 'EnableExchangeCloudManagement' -TenantFilter $Tenant -Preset Exchange if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEnableLitigationHold.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEnableLitigationHold.ps1 index 18ed72de321f..ca507e5c4ada 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEnableLitigationHold.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEnableLitigationHold.ps1 @@ -37,7 +37,7 @@ function Invoke-CIPPStandardEnableLitigationHold { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'EnableLitigationHold' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'EnableLitigationHold' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEnableMailTips.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEnableMailTips.ps1 index 8e466a01bd21..8b7e8b2c958a 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEnableMailTips.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEnableMailTips.ps1 @@ -41,7 +41,7 @@ function Invoke-CIPPStandardEnableMailTips { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'EnableMailTips' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'EnableMailTips' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEnableMailboxAuditing.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEnableMailboxAuditing.ps1 index e860f83158b6..9f34f19c89e0 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEnableMailboxAuditing.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEnableMailboxAuditing.ps1 @@ -46,7 +46,7 @@ function Invoke-CIPPStandardEnableMailboxAuditing { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'EnableMailboxAuditing' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'EnableMailboxAuditing' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEnableOnlineArchiving.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEnableOnlineArchiving.ps1 index f636c6ddc721..be62e135c6dd 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEnableOnlineArchiving.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEnableOnlineArchiving.ps1 @@ -38,7 +38,7 @@ function Invoke-CIPPStandardEnableOnlineArchiving { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'EnableOnlineArchiving' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'EnableOnlineArchiving' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEnforcePrivateGroups.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEnforcePrivateGroups.ps1 index 6318cb73f1c1..18a682fbe48b 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEnforcePrivateGroups.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEnforcePrivateGroups.ps1 @@ -42,8 +42,7 @@ function Invoke-CIPPStandardEnforcePrivateGroups { param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'EnforcePrivateGroups' -TenantFilter $Tenant ` - -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'SHAREPOINTENTERPRISE_GOV', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') + $TestResult = Test-CIPPStandardLicense -StandardName 'EnforcePrivateGroups' -TenantFilter $Tenant -Preset SharePoint if ($TestResult -eq $false) { return $true } # Parse exclusion keywords from settings diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEnrollmentWindowsHelloForBusinessConfiguration.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEnrollmentWindowsHelloForBusinessConfiguration.ps1 index 6aeef7791f42..b3328394d715 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEnrollmentWindowsHelloForBusinessConfiguration.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardEnrollmentWindowsHelloForBusinessConfiguration.ps1 @@ -48,7 +48,7 @@ function Invoke-CIPPStandardEnrollmentWindowsHelloForBusinessConfiguration { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'EnrollmentWindowsHelloForBusinessConfiguration' -TenantFilter $Tenant -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') + $TestResult = Test-CIPPStandardLicense -StandardName 'EnrollmentWindowsHelloForBusinessConfiguration' -TenantFilter $Tenant -Preset Intune if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardExchangeConnectorTemplate.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardExchangeConnectorTemplate.ps1 index c987dbce8c8c..5656ab3f1bd5 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardExchangeConnectorTemplate.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardExchangeConnectorTemplate.ps1 @@ -34,7 +34,7 @@ function Invoke-CIPPStandardExchangeConnectorTemplate { https://docs.cipp.app/user-documentation/tenant/standards/alignment/templates/available-standards #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'ExConnector' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'ExConnector' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardExcludedfileExt.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardExcludedfileExt.ps1 index 8122d47ef442..04bf7499b5e7 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardExcludedfileExt.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardExcludedfileExt.ps1 @@ -39,7 +39,7 @@ function Invoke-CIPPStandardExcludedfileExt { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'ExcludedfileExt' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'SHAREPOINTENTERPRISE_GOV', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') + $TestResult = Test-CIPPStandardLicense -StandardName 'ExcludedfileExt' -TenantFilter $Tenant -Preset SharePoint if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardExternalMFATrusted.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardExternalMFATrusted.ps1 index 1ab15ee45e88..d87b9e22c26f 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardExternalMFATrusted.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardExternalMFATrusted.ps1 @@ -36,7 +36,7 @@ function Invoke-CIPPStandardExternalMFATrusted { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'ExternalMFATrusted' -TenantFilter $Tenant -RequiredCapabilities @('AAD_PREMIUM', 'AAD_PREMIUM_P2') + $TestResult = Test-CIPPStandardLicense -StandardName 'ExternalMFATrusted' -TenantFilter $Tenant -Preset Entra if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardFocusedInbox.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardFocusedInbox.ps1 index 8e57cd541f97..823750b3ef0e 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardFocusedInbox.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardFocusedInbox.ps1 @@ -37,7 +37,7 @@ function Invoke-CIPPStandardFocusedInbox { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'FocusedInbox' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'FocusedInbox' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardGlobalQuarantineNotifications.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardGlobalQuarantineNotifications.ps1 index 3ac6a729e2cd..de4176e64cf1 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardGlobalQuarantineNotifications.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardGlobalQuarantineNotifications.ps1 @@ -34,7 +34,7 @@ function Invoke-CIPPStandardGlobalQuarantineNotifications { https://docs.cipp.app/user-documentation/tenant/standards/alignment/templates/available-standards #> param ($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'GlobalQuarantineNotifications' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'GlobalQuarantineNotifications' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardGlobalQuarantineSettings.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardGlobalQuarantineSettings.ps1 index a246bdcc200d..2785b1fe9443 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardGlobalQuarantineSettings.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardGlobalQuarantineSettings.ps1 @@ -40,7 +40,7 @@ function Invoke-CIPPStandardGlobalQuarantineSettings { https://docs.cipp.app/user-documentation/tenant/standards/alignment/templates/available-standards #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'QuarantineTemplate' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'QuarantineTemplate' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 index 4c7e40d2deb3..de45554c3c5b 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 @@ -67,7 +67,7 @@ function Invoke-CIPPStandardGroupTemplate { # Check if Exchange license is required for distribution groups if ($groupobj.groupType -in @('distribution', 'dynamicdistribution')) { - $TestResult = Test-CIPPStandardLicense -StandardName 'GroupTemplate' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') -SkipLog + $TestResult = Test-CIPPStandardLicense -StandardName 'GroupTemplate' -TenantFilter $Tenant -Preset Exchange -SkipLog if (!$TestResult) { Write-LogMessage -API 'Standards' -tenant $tenant -message "Cannot create group $($groupobj.displayname) as the tenant is not licensed for Exchange." -Sev 'Error' continue @@ -132,7 +132,7 @@ function Invoke-CIPPStandardGroupTemplate { } else { # Handle Exchange Online groups (Distribution, DynamicDistribution) - $TestResult = Test-CIPPStandardLicense -StandardName 'GroupTemplate' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') -SkipLog + $TestResult = Test-CIPPStandardLicense -StandardName 'GroupTemplate' -TenantFilter $Tenant -Preset Exchange -SkipLog if (!$TestResult) { Write-LogMessage -API 'Standards' -tenant $tenant -message "Cannot update group $($groupobj.displayName) as the tenant is not licensed for Exchange." -Sev 'Error' continue diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardIntuneComplianceSettings.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardIntuneComplianceSettings.ps1 index df487937539a..8baa105848d6 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardIntuneComplianceSettings.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardIntuneComplianceSettings.ps1 @@ -38,7 +38,7 @@ function Invoke-CIPPStandardIntuneComplianceSettings { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'IntuneComplianceSettings' -TenantFilter $Tenant -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') + $TestResult = Test-CIPPStandardLicense -StandardName 'IntuneComplianceSettings' -TenantFilter $Tenant -Preset Intune if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardIntuneWindowsDiagnostic.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardIntuneWindowsDiagnostic.ps1 index 92d909ca3405..1d1c05158750 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardIntuneWindowsDiagnostic.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardIntuneWindowsDiagnostic.ps1 @@ -39,7 +39,7 @@ Function Invoke-CIPPStandardIntuneWindowsDiagnostic { [CmdletBinding()] param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'IntuneWindowsDiagnostic' -TenantFilter $Tenant -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') + $TestResult = Test-CIPPStandardLicense -StandardName 'IntuneWindowsDiagnostic' -TenantFilter $Tenant -Preset Intune if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardMDMEnrollmentDuringRegistration.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardMDMEnrollmentDuringRegistration.ps1 index 30069fbb830a..db17a2f05cc0 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardMDMEnrollmentDuringRegistration.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardMDMEnrollmentDuringRegistration.ps1 @@ -37,7 +37,7 @@ function Invoke-CIPPStandardMDMEnrollmentDuringRegistration { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'MDMEnrollmentDuringRegistration' -TenantFilter $Tenant -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') + $TestResult = Test-CIPPStandardLicense -StandardName 'MDMEnrollmentDuringRegistration' -TenantFilter $Tenant -Preset Intune if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardMDMScope.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardMDMScope.ps1 index 5098f8c1f676..01d9c241122b 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardMDMScope.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardMDMScope.ps1 @@ -38,7 +38,7 @@ function Invoke-CIPPStandardMDMScope { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'MDMScope' -TenantFilter $Tenant -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') + $TestResult = Test-CIPPStandardLicense -StandardName 'MDMScope' -TenantFilter $Tenant -Preset Intune if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1 index e3978dd418c4..f60b86449f21 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1 @@ -38,7 +38,7 @@ function Invoke-CIPPStandardMailboxRecipientLimits { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'MailboxRecipientLimits' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'MailboxRecipientLimits' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardMalwareFilterPolicy.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardMalwareFilterPolicy.ps1 index ce7fbc2faada..3d2e2ce054f2 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardMalwareFilterPolicy.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardMalwareFilterPolicy.ps1 @@ -58,7 +58,7 @@ function Invoke-CIPPStandardMalwareFilterPolicy { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'MalwareFilterPolicy' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'MalwareFilterPolicy' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardMessageExpiration.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardMessageExpiration.ps1 index ad43bb3ffe43..1bc199ba4537 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardMessageExpiration.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardMessageExpiration.ps1 @@ -34,7 +34,7 @@ function Invoke-CIPPStandardMessageExpiration { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'MessageExpiration' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'MessageExpiration' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardOMEBranding.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardOMEBranding.ps1 index ab7195227d0e..558d01163e2f 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardOMEBranding.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardOMEBranding.ps1 @@ -41,7 +41,7 @@ function Invoke-CIPPStandardOMEBranding { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'OMEBranding' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') + $TestResult = Test-CIPPStandardLicense -StandardName 'OMEBranding' -TenantFilter $Tenant -Preset Exchange if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardOWAAttachmentRestrictions.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardOWAAttachmentRestrictions.ps1 index ce0edb82308d..89d81c020bbe 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardOWAAttachmentRestrictions.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardOWAAttachmentRestrictions.ps1 @@ -42,7 +42,7 @@ function Invoke-CIPPStandardOWAAttachmentRestrictions { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'OWAAttachmentRestrictions' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'OWAAttachmentRestrictions' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardOutBoundSpamAlert.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardOutBoundSpamAlert.ps1 index 39cb11965a5d..e7879f4e62ad 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardOutBoundSpamAlert.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardOutBoundSpamAlert.ps1 @@ -37,7 +37,7 @@ function Invoke-CIPPStandardOutBoundSpamAlert { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'OutBoundSpamAlert' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'OutBoundSpamAlert' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 index 68f947447a7f..97e4306f878b 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 @@ -38,7 +38,7 @@ function Invoke-CIPPStandardPhishProtection { param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'PhishProtection' -TenantFilter $Tenant -RequiredCapabilities @('AAD_PREMIUM', 'AAD_PREMIUM_P2', 'OFFICE_BUSINESS') + $TestResult = Test-CIPPStandardLicense -StandardName 'PhishProtection' -TenantFilter $Tenant -Preset Entra -RequiredCapabilities @('OFFICE_BUSINESS') if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardPhishSimSpoofIntelligence.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardPhishSimSpoofIntelligence.ps1 index b7ff79e0be4b..2725af0a918a 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardPhishSimSpoofIntelligence.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardPhishSimSpoofIntelligence.ps1 @@ -36,7 +36,7 @@ function Invoke-CIPPStandardPhishSimSpoofIntelligence { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'PhishSimSpoofIntelligence' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'PhishSimSpoofIntelligence' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardPhishingSimulations.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardPhishingSimulations.ps1 index a7d16f46532e..2cd299511211 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardPhishingSimulations.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardPhishingSimulations.ps1 @@ -38,7 +38,7 @@ function Invoke-CIPPStandardPhishingSimulations { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'PhishingSimulations' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'PhishingSimulations' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardProfilePhotos.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardProfilePhotos.ps1 index a009e8f0acd1..8d7b5def8487 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardProfilePhotos.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardProfilePhotos.ps1 @@ -37,7 +37,7 @@ function Invoke-CIPPStandardProfilePhotos { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'ProfilePhotos' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'ProfilePhotos' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardQuarantineRequestAlert.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardQuarantineRequestAlert.ps1 index eeed05571d08..6bf45cd7ee53 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardQuarantineRequestAlert.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardQuarantineRequestAlert.ps1 @@ -37,7 +37,7 @@ function Invoke-CIPPStandardQuarantineRequestAlert { #> param ($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'QuarantineRequestAlert' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'QuarantineRequestAlert' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardQuarantineTemplate.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardQuarantineTemplate.ps1 index ff20224468dd..d0c0d0213682 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardQuarantineTemplate.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardQuarantineTemplate.ps1 @@ -48,7 +48,7 @@ function Invoke-CIPPStandardQuarantineTemplate { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'QuarantineTemplate' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'QuarantineTemplate' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardRestrictThirdPartyStorageServices.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardRestrictThirdPartyStorageServices.ps1 index 573a5ab8c48d..bb2db7e31eaa 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardRestrictThirdPartyStorageServices.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardRestrictThirdPartyStorageServices.ps1 @@ -40,7 +40,7 @@ function Invoke-CIPPStandardRestrictThirdPartyStorageServices { #> param ($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'ThirdPartyStorageServicesRestricted' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'SHAREPOINTENTERPRISE_GOV', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') + $TestResult = Test-CIPPStandardLicense -StandardName 'ThirdPartyStorageServicesRestricted' -TenantFilter $Tenant -Preset SharePoint if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardRetentionPolicyTag.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardRetentionPolicyTag.ps1 index b7b73b6042ea..aa9ede0c285b 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardRetentionPolicyTag.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardRetentionPolicyTag.ps1 @@ -38,7 +38,7 @@ function Invoke-CIPPStandardRetentionPolicyTag { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'RetentionPolicyTag' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'RetentionPolicyTag' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardReusableSettingsTemplate.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardReusableSettingsTemplate.ps1 index 4cec77dcda3e..c03242b0d0c7 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardReusableSettingsTemplate.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardReusableSettingsTemplate.ps1 @@ -25,7 +25,7 @@ function Invoke-CIPPStandardReusableSettingsTemplate { ADDEDCOMPONENT {"type":"autoComplete","multiple":true,"creatable":false,"required":true,"name":"TemplateList","label":"Select Reusable Settings Template","api":{"queryKey":"ListIntuneReusableSettingTemplates","url":"/api/ListIntuneReusableSettingTemplates","labelField":"displayName","valueField":"GUID","showRefresh":true,"templateView":{"title":"Reusable Settings","property":"RawJSON","type":"intune"}}} POWERSHELLEQUIVALENT - + RECOMMENDEDBY UPDATECOMMENTBLOCK Run the Tools\Update-StandardsComments.ps1 script to update this comment block @@ -62,8 +62,7 @@ function Invoke-CIPPStandardReusableSettingsTemplate { return $InputObject } - $RequiredCapabilities = @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') - $TestResult = Test-CIPPStandardLicense -StandardName 'ReusableSettingsTemplate_general' -TenantFilter $Tenant -RequiredCapabilities $RequiredCapabilities + $TestResult = Test-CIPPStandardLicense -StandardName 'ReusableSettingsTemplate_general' -TenantFilter $Tenant -Preset Intune if ($TestResult -eq $false) { $settings.TemplateList | ForEach-Object { $MissingLicenseMessage = "This tenant is missing one or more required licenses for this standard: $($RequiredCapabilities -join ', ')." diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardRotateDKIM.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardRotateDKIM.ps1 index ff8ffa317e8c..f2bf28002dee 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardRotateDKIM.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardRotateDKIM.ps1 @@ -39,7 +39,7 @@ function Invoke-CIPPStandardRotateDKIM { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'RotateDKIM' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'RotateDKIM' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPAzureB2B.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPAzureB2B.ps1 index 9c824d9996a1..334029b39daa 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPAzureB2B.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPAzureB2B.ps1 @@ -40,7 +40,7 @@ function Invoke-CIPPStandardSPAzureB2B { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'SPAzureB2B' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'SHAREPOINTENTERPRISE_GOV', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') + $TestResult = Test-CIPPStandardLicense -StandardName 'SPAzureB2B' -TenantFilter $Tenant -Preset SharePoint if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPDirectSharing.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPDirectSharing.ps1 index 9da12268a0ba..a138b2efec37 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPDirectSharing.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPDirectSharing.ps1 @@ -40,7 +40,7 @@ function Invoke-CIPPStandardSPDirectSharing { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'SPDirectSharing' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'SHAREPOINTENTERPRISE_GOV', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') + $TestResult = Test-CIPPStandardLicense -StandardName 'SPDirectSharing' -TenantFilter $Tenant -Preset SharePoint if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPDisableCustomScripts.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPDisableCustomScripts.ps1 index 7f5dfac5b923..805453d49c0d 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPDisableCustomScripts.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPDisableCustomScripts.ps1 @@ -39,7 +39,7 @@ function Invoke-CIPPStandardSPDisableCustomScripts { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'SPDisableCustomScripts' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'SHAREPOINTENTERPRISE_GOV', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') + $TestResult = Test-CIPPStandardLicense -StandardName 'SPDisableCustomScripts' -TenantFilter $Tenant -Preset SharePoint if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPDisableLegacyWorkflows.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPDisableLegacyWorkflows.ps1 index 0945cf6e4047..49c13f27f511 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPDisableLegacyWorkflows.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPDisableLegacyWorkflows.ps1 @@ -37,7 +37,7 @@ function Invoke-CIPPStandardSPDisableLegacyWorkflows { https://docs.cipp.app/user-documentation/tenant/standards/alignment/templates/available-standards #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'SPDisableLegacyWorkflows' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'SHAREPOINTENTERPRISE_GOV', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') + $TestResult = Test-CIPPStandardLicense -StandardName 'SPDisableLegacyWorkflows' -TenantFilter $Tenant -Preset SharePoint if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPDisableStoreAccess.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPDisableStoreAccess.ps1 index ae770fa8b793..0339550bcad7 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPDisableStoreAccess.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPDisableStoreAccess.ps1 @@ -39,7 +39,7 @@ function Invoke-CIPPStandardSPDisableStoreAccess { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'SPDisableStoreAccess' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'SHAREPOINTENTERPRISE_GOV', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') + $TestResult = Test-CIPPStandardLicense -StandardName 'SPDisableStoreAccess' -TenantFilter $Tenant -Preset SharePoint if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPDisallowInfectedFiles.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPDisallowInfectedFiles.ps1 index d83e676965ca..92b624ffb2bd 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPDisallowInfectedFiles.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPDisallowInfectedFiles.ps1 @@ -44,7 +44,7 @@ function Invoke-CIPPStandardSPDisallowInfectedFiles { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'SPDisallowInfectedFiles' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'SHAREPOINTENTERPRISE_GOV','ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') + $TestResult = Test-CIPPStandardLicense -StandardName 'SPDisallowInfectedFiles' -TenantFilter $Tenant -Preset SharePoint if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPEmailAttestation.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPEmailAttestation.ps1 index f989f5451ada..ade01fe5781a 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPEmailAttestation.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPEmailAttestation.ps1 @@ -45,7 +45,7 @@ function Invoke-CIPPStandardSPEmailAttestation { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'SPEmailAttestation' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'SHAREPOINTENTERPRISE_GOV', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') + $TestResult = Test-CIPPStandardLicense -StandardName 'SPEmailAttestation' -TenantFilter $Tenant -Preset SharePoint if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPExternalUserExpiration.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPExternalUserExpiration.ps1 index b6e9c3d2fd4a..90ca95ac03f8 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPExternalUserExpiration.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPExternalUserExpiration.ps1 @@ -45,7 +45,7 @@ function Invoke-CIPPStandardSPExternalUserExpiration { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'SPExternalUserExpiration' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'SHAREPOINTENTERPRISE_GOV', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') + $TestResult = Test-CIPPStandardLicense -StandardName 'SPExternalUserExpiration' -TenantFilter $Tenant -Preset SharePoint if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPFileRequests.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPFileRequests.ps1 index 24f821847e2d..78a90454f745 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPFileRequests.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPFileRequests.ps1 @@ -41,7 +41,7 @@ function Invoke-CIPPStandardSPFileRequests { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'SPFileRequests' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'SHAREPOINTENTERPRISE_GOV', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') + $TestResult = Test-CIPPStandardLicense -StandardName 'SPFileRequests' -TenantFilter $Tenant -Preset SharePoint if ($TestResult -eq $false) { Write-LogMessage -API 'Standards' -tenant $tenant -message 'The tenant is not licenced for this standard SPFileRequests' -sev Error diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPSyncButtonState.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPSyncButtonState.ps1 index 4b08283dac8d..694770223d2f 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPSyncButtonState.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPSyncButtonState.ps1 @@ -39,7 +39,7 @@ function Invoke-CIPPStandardSPSyncButtonState { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'SPSyncButtonState' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'SHAREPOINTENTERPRISE_GOV', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') + $TestResult = Test-CIPPStandardLicense -StandardName 'SPSyncButtonState' -TenantFilter $Tenant -Preset SharePoint if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSafeAttachmentPolicy.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSafeAttachmentPolicy.ps1 index ac5ab95dbff1..61b515a26761 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSafeAttachmentPolicy.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSafeAttachmentPolicy.ps1 @@ -47,13 +47,13 @@ function Invoke-CIPPStandardSafeAttachmentPolicy { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'SafeAttachmentPolicy' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'SafeAttachmentPolicy' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true } #we're done. - $MDOTestResult = Test-CIPPStandardLicense -StandardName 'SafeAttachmentPolicy' -TenantFilter $Tenant -RequiredCapabilities @('ATP_ENTERPRISE', 'ATP_ENTERPRISE_GOV', 'THREAT_INTELLIGENCE') + $MDOTestResult = Test-CIPPStandardLicense -StandardName 'SafeAttachmentPolicy' -TenantFilter $Tenant -Preset DefenderForOffice365 if ($MDOTestResult -eq $false) { return $true @@ -113,8 +113,8 @@ function Invoke-CIPPStandardSafeAttachmentPolicy { $AcceptedDomains = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-AcceptedDomain' $RuleState = $AllSafeAttachmentRule | - Where-Object -Property Name -EQ $RuleName | - Select-Object Name, SafeAttachmentPolicy, Priority, RecipientDomainIs + Where-Object -Property Name -EQ $RuleName | + Select-Object Name, SafeAttachmentPolicy, Priority, RecipientDomainIs $RuleStateIsCorrect = ($RuleState.Name -eq $RuleName) -and ($RuleState.SafeAttachmentPolicy -eq $PolicyName) -and diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSafeLinksPolicy.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSafeLinksPolicy.ps1 index 742704fe886f..3e10dc15f1d8 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSafeLinksPolicy.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSafeLinksPolicy.ps1 @@ -61,13 +61,13 @@ function Invoke-CIPPStandardSafeLinksPolicy { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'SafeLinksPolicy' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'SafeLinksPolicy' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true } #we're done. - $MDOTestResult = Test-CIPPStandardLicense -StandardName 'SafeLinksPolicy' -TenantFilter $Tenant -RequiredCapabilities @('ATP_ENTERPRISE', 'ATP_ENTERPRISE_GOV', 'THREAT_INTELLIGENCE') + $MDOTestResult = Test-CIPPStandardLicense -StandardName 'SafeLinksPolicy' -TenantFilter $Tenant -Preset DefenderForOffice365 if ($MDOTestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSafeLinksTemplatePolicy.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSafeLinksTemplatePolicy.ps1 index 0f0f703d6724..475de0053c67 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSafeLinksTemplatePolicy.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSafeLinksTemplatePolicy.ps1 @@ -37,7 +37,7 @@ function Invoke-CIPPStandardSafeLinksTemplatePolicy { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'SafeLinksTemplatePolicy' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'SafeLinksTemplatePolicy' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true @@ -45,7 +45,7 @@ function Invoke-CIPPStandardSafeLinksTemplatePolicy { Write-LogMessage -API 'Standards' -tenant $Tenant -message "Processing SafeLinks template with settings: $($Settings | ConvertTo-Json -Compress)" -sev Debug - $MDOTestResult = Test-CIPPStandardLicense -StandardName 'SafeLinksTemplatePolicy' -TenantFilter $Tenant -RequiredCapabilities @('ATP_ENTERPRISE', 'ATP_ENTERPRISE_GOV', 'THREAT_INTELLIGENCE') + $MDOTestResult = Test-CIPPStandardLicense -StandardName 'SafeLinksTemplatePolicy' -TenantFilter $Tenant -Preset DefenderForOffice365 if ($MDOTestResult -eq $false) { return } #tenant lacks Microsoft Defender for Office 365 — Test-CIPPStandardLicense logs and sets LicenseAvailable=false. diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSafeSendersDisable.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSafeSendersDisable.ps1 index afbc4d0ee03d..de271d07ab10 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSafeSendersDisable.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSafeSendersDisable.ps1 @@ -39,7 +39,7 @@ function Invoke-CIPPStandardSafeSendersDisable { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'SafeSendersDisable' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'SafeSendersDisable' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSendFromAlias.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSendFromAlias.ps1 index c5c269dcb31e..d6fd2c15157f 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSendFromAlias.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSendFromAlias.ps1 @@ -38,7 +38,7 @@ function Invoke-CIPPStandardSendFromAlias { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'SendFromAlias' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'SendFromAlias' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSendReceiveLimitTenant.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSendReceiveLimitTenant.ps1 index f10fc0222928..0443552cd728 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSendReceiveLimitTenant.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSendReceiveLimitTenant.ps1 @@ -38,7 +38,7 @@ function Invoke-CIPPStandardSendReceiveLimitTenant { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'SendReceiveLimitTenant' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'SendReceiveLimitTenant' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardShortenMeetings.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardShortenMeetings.ps1 index fe1d4b98cade..c9f1b96850cd 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardShortenMeetings.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardShortenMeetings.ps1 @@ -39,7 +39,7 @@ function Invoke-CIPPStandardShortenMeetings { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'ShortenMeetings' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'ShortenMeetings' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 index 02de560f8713..f01a369d3406 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 @@ -75,7 +75,7 @@ function Invoke-CIPPStandardSpamFilterPolicy { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'SpamFilterPolicy' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'SpamFilterPolicy' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true @@ -86,7 +86,7 @@ function Invoke-CIPPStandardSpamFilterPolicy { try { $CurrentState = New-ExoRequest -TenantId $Tenant -cmdlet 'Get-HostedContentFilterPolicy' | - Where-Object -Property Name -EQ $PolicyName + Where-Object -Property Name -EQ $PolicyName } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the SpamFilterPolicy state for $Tenant. Error: $ErrorMessage" -Sev Error @@ -157,10 +157,11 @@ function Invoke-CIPPStandardSpamFilterPolicy { $AcceptedDomains = New-ExoRequest -TenantId $Tenant -cmdlet 'Get-AcceptedDomain' $RuleState = New-ExoRequest -TenantId $Tenant -cmdlet 'Get-HostedContentFilterRule' | - Where-Object -Property Name -EQ $PolicyName + Where-Object -Property Name -EQ $PolicyName $RuleStateIsCorrect = ($RuleState.Name -eq $PolicyName) -and ($RuleState.HostedContentFilterPolicy -eq $PolicyName) -and + ($RuleState.State -eq 'Enabled') -and ($RuleState.Priority -eq 0) -and (!(Compare-Object -ReferenceObject $RuleState.RecipientDomainIs -DifferenceObject $AcceptedDomains.Name)) @@ -255,6 +256,15 @@ function Invoke-CIPPStandardSpamFilterPolicy { } catch { Write-LogMessage -API 'Standards' -Tenant $Tenant -message "Failed to update Spam Filter rule $PolicyName." -sev Error -LogData $_ } + + if ($RuleState.State -eq 'Disabled') { + try { + $null = New-ExoRequest -TenantId $Tenant -cmdlet 'Enable-HostedContentFilterRule' -cmdParams @{ Identity = $PolicyName } -UseSystemMailbox $true + Write-LogMessage -API 'Standards' -Tenant $Tenant -message "Enabled Spam Filter rule $PolicyName." -sev Info + } catch { + Write-LogMessage -API 'Standards' -Tenant $Tenant -message "Failed to enable Spam Filter rule $PolicyName." -sev Error -LogData $_ + } + } } else { try { $cmdParams.Add('Name', "$PolicyName") diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSpoofWarn.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSpoofWarn.ps1 index 39d6ca1865b2..ff57beeb65a1 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSpoofWarn.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSpoofWarn.ps1 @@ -44,7 +44,7 @@ function Invoke-CIPPStandardSpoofWarn { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'SpoofWarn' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'SpoofWarn' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardStaleEntraDevices.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardStaleEntraDevices.ps1 index b48b48fc2fc3..3ad06b586dea 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardStaleEntraDevices.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardStaleEntraDevices.ps1 @@ -42,7 +42,7 @@ function Invoke-CIPPStandardStaleEntraDevices { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'StaleEntraDevices' -TenantFilter $Tenant -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') + $TestResult = Test-CIPPStandardLicense -StandardName 'StaleEntraDevices' -TenantFilter $Tenant -Preset Intune # Get all Entra devices diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsChatProtection.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsChatProtection.ps1 index 826f5e5a26b4..93eccf035e10 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsChatProtection.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsChatProtection.ps1 @@ -39,7 +39,7 @@ function Invoke-CIPPStandardTeamsChatProtection { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsChatProtection' -TenantFilter $Tenant -RequiredCapabilities @('MCOSTANDARD', 'MCOEV', 'MCOIMP', 'TEAMS1', 'Teams_Room_Standard') + $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsChatProtection' -TenantFilter $Tenant -Preset Teams if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsEmailIntegration.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsEmailIntegration.ps1 index d3bc8653f835..fe913f850dac 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsEmailIntegration.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsEmailIntegration.ps1 @@ -39,7 +39,7 @@ function Invoke-CIPPStandardTeamsEmailIntegration { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsEmailIntegration' -TenantFilter $Tenant -RequiredCapabilities @('MCOSTANDARD', 'MCOEV', 'MCOIMP', 'TEAMS1', 'Teams_Room_Standard') + $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsEmailIntegration' -TenantFilter $Tenant -Preset Teams if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsEnrollUser.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsEnrollUser.ps1 index 000c47e283ba..3748d8d67a03 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsEnrollUser.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsEnrollUser.ps1 @@ -37,7 +37,7 @@ function Invoke-CIPPStandardTeamsEnrollUser { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsEnrollUser' -TenantFilter $Tenant -RequiredCapabilities @('MCOSTANDARD', 'MCOEV', 'MCOIMP', 'TEAMS1', 'Teams_Room_Standard') + $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsEnrollUser' -TenantFilter $Tenant -Preset Teams # Get EnrollUserOverride value using null-coalescing operator diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsExternalAccessPolicy.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsExternalAccessPolicy.ps1 index 4055445346a6..a6014c5fec2a 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsExternalAccessPolicy.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsExternalAccessPolicy.ps1 @@ -38,7 +38,7 @@ function Invoke-CIPPStandardTeamsExternalAccessPolicy { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsExternalAccessPolicy' -TenantFilter $Tenant -RequiredCapabilities @('MCOSTANDARD', 'MCOEV', 'MCOIMP', 'TEAMS1', 'Teams_Room_Standard') + $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsExternalAccessPolicy' -TenantFilter $Tenant -Preset Teams if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsExternalChatWithAnyone.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsExternalChatWithAnyone.ps1 index 11e974546851..f4772514fb92 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsExternalChatWithAnyone.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsExternalChatWithAnyone.ps1 @@ -38,7 +38,7 @@ function Invoke-CIPPStandardTeamsExternalChatWithAnyone { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsExternalChatWithAnyone' -TenantFilter $Tenant -RequiredCapabilities @('MCOSTANDARD', 'MCOEV', 'MCOIMP', 'TEAMS1', 'Teams_Room_Standard') + $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsExternalChatWithAnyone' -TenantFilter $Tenant -Preset Teams if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsExternalFileSharing.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsExternalFileSharing.ps1 index f267e5f63c62..1d29eb41f6fa 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsExternalFileSharing.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsExternalFileSharing.ps1 @@ -43,7 +43,7 @@ function Invoke-CIPPStandardTeamsExternalFileSharing { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsExternalFileSharing' -TenantFilter $Tenant -RequiredCapabilities @('MCOSTANDARD', 'MCOEV', 'MCOIMP', 'TEAMS1', 'Teams_Room_Standard') + $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsExternalFileSharing' -TenantFilter $Tenant -Preset Teams if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsFederationConfiguration.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsFederationConfiguration.ps1 index 8e6f9036ff5a..4bebc295e2ee 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsFederationConfiguration.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsFederationConfiguration.ps1 @@ -39,7 +39,7 @@ function Invoke-CIPPStandardTeamsFederationConfiguration { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsFederationConfiguration' -TenantFilter $Tenant -RequiredCapabilities @('MCOSTANDARD', 'MCOEV', 'MCOIMP', 'TEAMS1', 'Teams_Room_Standard') + $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsFederationConfiguration' -TenantFilter $Tenant -Preset Teams if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsGlobalMeetingPolicy.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsGlobalMeetingPolicy.ps1 index 382a2216e988..64f368e07dbc 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsGlobalMeetingPolicy.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsGlobalMeetingPolicy.ps1 @@ -50,7 +50,7 @@ function Invoke-CIPPStandardTeamsGlobalMeetingPolicy { https://docs.cipp.app/user-documentation/tenant/standards/alignment/templates/available-standards #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsGlobalMeetingPolicy' -TenantFilter $Tenant -RequiredCapabilities @('MCOSTANDARD', 'MCOEV', 'MCOIMP', 'TEAMS1', 'Teams_Room_Standard') + $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsGlobalMeetingPolicy' -TenantFilter $Tenant -Preset Teams if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsGuestAccess.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsGuestAccess.ps1 index 53a8c8a33f71..082638fd09fd 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsGuestAccess.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsGuestAccess.ps1 @@ -37,7 +37,7 @@ function Invoke-CIPPStandardTeamsGuestAccess { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsGuestAccess' -TenantFilter $Tenant -RequiredCapabilities @('MCOSTANDARD', 'MCOEV', 'MCOIMP', 'TEAMS1', 'Teams_Room_Standard') + $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsGuestAccess' -TenantFilter $Tenant -Preset Teams if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsMeetingRecordingExpiration.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsMeetingRecordingExpiration.ps1 index 3e17c4090870..d1ae1626cafb 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsMeetingRecordingExpiration.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsMeetingRecordingExpiration.ps1 @@ -37,7 +37,7 @@ function Invoke-CIPPStandardTeamsMeetingRecordingExpiration { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsMeetingRecordingExpiration' -TenantFilter $Tenant -RequiredCapabilities @('MCOSTANDARD', 'MCOEV', 'MCOIMP', 'TEAMS1', 'Teams_Room_Standard') + $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsMeetingRecordingExpiration' -TenantFilter $Tenant -Preset Teams # Input validation diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsMeetingVerification.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsMeetingVerification.ps1 index 30118bed5e2b..e13384d13c8d 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsMeetingVerification.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsMeetingVerification.ps1 @@ -38,7 +38,7 @@ function Invoke-CIPPStandardTeamsMeetingVerification { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsMeetingVerification' -TenantFilter $Tenant -RequiredCapabilities @('MCOSTANDARD', 'MCOEV', 'MCOIMP', 'TEAMS1', 'Teams_Room_Standard') + $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsMeetingVerification' -TenantFilter $Tenant -Preset Teams if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsMeetingsByDefault.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsMeetingsByDefault.ps1 index 89aa5d85e5e3..5549004d5028 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsMeetingsByDefault.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsMeetingsByDefault.ps1 @@ -37,7 +37,7 @@ function Invoke-CIPPStandardTeamsMeetingsByDefault { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsMeetingsByDefault' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsMeetingsByDefault' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsMessagingPolicy.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsMessagingPolicy.ps1 index c264233f6cb8..31a6384293c4 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsMessagingPolicy.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsMessagingPolicy.ps1 @@ -45,7 +45,7 @@ function Invoke-CIPPStandardTeamsMessagingPolicy { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsMessagingPolicy' -TenantFilter $Tenant -RequiredCapabilities @('MCOSTANDARD', 'MCOEV', 'MCOIMP', 'TEAMS1', 'Teams_Room_Standard') + $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsMessagingPolicy' -TenantFilter $Tenant -Preset Teams if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsZAP.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsZAP.ps1 index 3b8b0ea9b22d..7b3c19c146d2 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsZAP.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsZAP.ps1 @@ -39,7 +39,7 @@ function Invoke-CIPPStandardTeamsZAP { param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsZAP' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') + $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsZAP' -TenantFilter $Tenant -Preset Exchange if ($TestResult -eq $false) { return $true } try { diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTenantAllowBlockListTemplate.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTenantAllowBlockListTemplate.ps1 index 2b1459c31275..5efb88b9210d 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTenantAllowBlockListTemplate.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTenantAllowBlockListTemplate.ps1 @@ -35,7 +35,7 @@ function Invoke-CIPPStandardTenantAllowBlockListTemplate { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'TenantAllowBlockListTemplate' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') + $TestResult = Test-CIPPStandardLicense -StandardName 'TenantAllowBlockListTemplate' -TenantFilter $Tenant -Preset Exchange if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTenantDefaultTimezone.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTenantDefaultTimezone.ps1 index 803b905e1ff6..ec5836391ad5 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTenantDefaultTimezone.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTenantDefaultTimezone.ps1 @@ -39,7 +39,7 @@ function Invoke-CIPPStandardTenantDefaultTimezone { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'TenantDefaultTimezone' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'SHAREPOINTENTERPRISE_GOV', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') + $TestResult = Test-CIPPStandardLicense -StandardName 'TenantDefaultTimezone' -TenantFilter $Tenant -Preset SharePoint if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTransportRuleTemplate.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTransportRuleTemplate.ps1 index 5a757cacccf9..4c6801d28e02 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTransportRuleTemplate.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTransportRuleTemplate.ps1 @@ -35,7 +35,7 @@ function Invoke-CIPPStandardTransportRuleTemplate { https://docs.cipp.app/user-documentation/tenant/standards/alignment/templates/available-standards #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'TransportRuleTemplate' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'TransportRuleTemplate' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTwoClickEmailProtection.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTwoClickEmailProtection.ps1 index 61336c9964e2..91e88460f848 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTwoClickEmailProtection.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTwoClickEmailProtection.ps1 @@ -37,7 +37,7 @@ function Invoke-CIPPStandardTwoClickEmailProtection { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'TwoClickEmailProtection' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'TwoClickEmailProtection' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardUserSubmissions.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardUserSubmissions.ps1 index 834f49c502b8..7c5c3d8c8e16 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardUserSubmissions.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardUserSubmissions.ps1 @@ -38,7 +38,7 @@ function Invoke-CIPPStandardUserSubmissions { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'UserSubmissions' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'UserSubmissions' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardWindowsBackupRestore.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardWindowsBackupRestore.ps1 index df798e9a854b..3f03cb9cad1c 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardWindowsBackupRestore.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardWindowsBackupRestore.ps1 @@ -39,7 +39,7 @@ function Invoke-CIPPStandardWindowsBackupRestore { [CmdletBinding()] param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'WindowsBackupRestore' -TenantFilter $Tenant -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') + $TestResult = Test-CIPPStandardLicense -StandardName 'WindowsBackupRestore' -TenantFilter $Tenant -Preset Intune if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardcalDefault.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardcalDefault.ps1 index 389de7084762..6a7d9836a392 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardcalDefault.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardcalDefault.ps1 @@ -40,7 +40,7 @@ function Invoke-CIPPStandardcalDefault { param($Tenant, $Settings, $QueueItem) ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'calDefault' - $TestResult = Test-CIPPStandardLicense -StandardName 'calDefault' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'calDefault' -TenantFilter $Tenant -Preset Exchange #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandarddisableMacSync.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandarddisableMacSync.ps1 index c0263f63f4b2..f5373e945d40 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandarddisableMacSync.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandarddisableMacSync.ps1 @@ -38,7 +38,7 @@ function Invoke-CIPPStandarddisableMacSync { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'disableMacSync' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'SHAREPOINTENTERPRISE_GOV', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') + $TestResult = Test-CIPPStandardLicense -StandardName 'disableMacSync' -TenantFilter $Tenant -Preset SharePoint if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardintuneBrandingProfile.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardintuneBrandingProfile.ps1 index f99e7c48d9c6..2624bed22cdc 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardintuneBrandingProfile.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardintuneBrandingProfile.ps1 @@ -46,7 +46,7 @@ function Invoke-CIPPStandardintuneBrandingProfile { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'intuneBrandingProfile' -TenantFilter $Tenant -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') + $TestResult = Test-CIPPStandardLicense -StandardName 'intuneBrandingProfile' -TenantFilter $Tenant -Preset Intune if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardintuneDeviceReg.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardintuneDeviceReg.ps1 index 2e32e5f8cca9..ec7dd58175c8 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardintuneDeviceReg.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardintuneDeviceReg.ps1 @@ -40,7 +40,7 @@ function Invoke-CIPPStandardintuneDeviceReg { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'intuneDeviceReg' -TenantFilter $Tenant -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') + $TestResult = Test-CIPPStandardLicense -StandardName 'intuneDeviceReg' -TenantFilter $Tenant -Preset Intune if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardintuneDeviceRetirementDays.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardintuneDeviceRetirementDays.ps1 index e2958d7fef17..d0184eabe354 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardintuneDeviceRetirementDays.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardintuneDeviceRetirementDays.ps1 @@ -38,7 +38,7 @@ function Invoke-CIPPStandardintuneDeviceRetirementDays { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'intuneDeviceRetirementDays' -TenantFilter $Tenant -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') + $TestResult = Test-CIPPStandardLicense -StandardName 'intuneDeviceRetirementDays' -TenantFilter $Tenant -Preset Intune if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardsharingCapability.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardsharingCapability.ps1 index c0ba257ab116..33c7b9209a1b 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardsharingCapability.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardsharingCapability.ps1 @@ -46,7 +46,7 @@ function Invoke-CIPPStandardsharingCapability { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'sharingCapability' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'SHAREPOINTENTERPRISE_GOV', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') + $TestResult = Test-CIPPStandardLicense -StandardName 'sharingCapability' -TenantFilter $Tenant -Preset SharePoint if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardsharingDomainRestriction.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardsharingDomainRestriction.ps1 index bccd721bdf66..6bec4a3e9c6c 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardsharingDomainRestriction.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardsharingDomainRestriction.ps1 @@ -45,7 +45,7 @@ function Invoke-CIPPStandardsharingDomainRestriction { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'sharingDomainRestriction' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'SHAREPOINTENTERPRISE_GOV', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') + $TestResult = Test-CIPPStandardLicense -StandardName 'sharingDomainRestriction' -TenantFilter $Tenant -Preset SharePoint if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardunmanagedSync.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardunmanagedSync.ps1 index b48002c97a0f..23c1207c5fc4 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardunmanagedSync.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardunmanagedSync.ps1 @@ -42,7 +42,7 @@ function Invoke-CIPPStandardunmanagedSync { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'unmanagedSync' -TenantFilter $Tenant -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') + $TestResult = Test-CIPPStandardLicense -StandardName 'unmanagedSync' -TenantFilter $Tenant -Preset Intune if ($TestResult -eq $false) { return $true diff --git a/Tools/Update-StandardsComments.ps1 b/Tools/Update-StandardsComments.ps1 index 6d92b2e60649..b7ff54a7e933 100644 --- a/Tools/Update-StandardsComments.ps1 +++ b/Tools/Update-StandardsComments.ps1 @@ -47,9 +47,137 @@ function EscapeMarkdown([object]$InputObject) { return $Temp.Replace('\', '\\').Replace('*', '\*').Replace('_', '\_').Replace("``", "\``").Replace('$', '\$').Replace('|', '\|').Replace('<', '\<').Replace('>', '\>').Replace([System.Environment]::NewLine, '
') } +function Get-StringValuesFromAst { + param( + [Parameter(Mandatory = $true)] + [System.Management.Automation.Language.Ast]$ArgumentAst + ) + + try { + $Value = $ArgumentAst.SafeGetValue() + if ($Value -is [array]) { + return @($Value | ForEach-Object { $_.ToString() }) + } + + if ($Value -is [string]) { + return @($Value) + } + } catch {} + + if ($ArgumentAst -is [System.Management.Automation.Language.StringConstantExpressionAst]) { + return @($ArgumentAst.Value) + } + + if ($ArgumentAst -is [System.Management.Automation.Language.ExpandableStringExpressionAst]) { + return @($ArgumentAst.Value) + } + + if ($ArgumentAst -is [System.Management.Automation.Language.ArrayLiteralAst]) { + return @($ArgumentAst.Elements | ForEach-Object { Get-StringValuesFromAst -ArgumentAst $_ }) + } + + if ($ArgumentAst -is [System.Management.Automation.Language.PipelineAst]) { + return @($ArgumentAst.PipelineElements | ForEach-Object { Get-StringValuesFromAst -ArgumentAst $_ }) + } + + if ($ArgumentAst -is [System.Management.Automation.Language.CommandExpressionAst]) { + return @(Get-StringValuesFromAst -ArgumentAst $ArgumentAst.Expression) + } + + return @() +} + +function Get-CIPPCapabilityPresets { + param( + [Parameter(Mandatory = $true)] + [string]$Path + ) + + $Tokens = $null + $ParseErrors = $null + $Ast = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$Tokens, [ref]$ParseErrors) + $LicenseFunction = $Ast.Find({ + param($Node) + $Node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and $Node.Name -eq 'Test-CIPPStandardLicense' + }, $true) + + $PresetAssignment = $LicenseFunction.Body.Find({ + param($Node) + $Node -is [System.Management.Automation.Language.AssignmentStatementAst] -and + $Node.Left -is [System.Management.Automation.Language.VariableExpressionAst] -and + $Node.Left.VariablePath.UserPath -eq 'Presets' -and + $Node.Right -is [System.Management.Automation.Language.HashtableAst] + }, $true) + + $Presets = @{} + foreach ($Pair in $PresetAssignment.Right.KeyValuePairs) { + $PresetName = (Get-StringValuesFromAst -ArgumentAst $Pair.Item1)[0] + $Presets[$PresetName] = @(Get-StringValuesFromAst -ArgumentAst $Pair.Item2) + } + + return $Presets +} + +function Get-LicenseCheckCapabilities { + param( + [Parameter(Mandatory = $true)] + [string]$Content, + + [Parameter(Mandatory = $true)] + [hashtable]$CapabilityPresets + ) + + $Tokens = $null + $ParseErrors = $null + $Ast = [System.Management.Automation.Language.Parser]::ParseInput($Content, [ref]$Tokens, [ref]$ParseErrors) + $LicenseCheck = $Ast.Find({ + param($Node) + $Node -is [System.Management.Automation.Language.CommandAst] -and $Node.GetCommandName() -eq 'Test-CIPPStandardLicense' + }, $true) + + if (!$LicenseCheck) { + return @() + } + + $Capabilities = [System.Collections.Generic.List[string]]::new() + for ($Index = 0; $Index -lt $LicenseCheck.CommandElements.Count; $Index++) { + $Element = $LicenseCheck.CommandElements[$Index] + if ($Element -isnot [System.Management.Automation.Language.CommandParameterAst]) { + continue + } + + $Argument = if (($Index + 1) -lt $LicenseCheck.CommandElements.Count -and $LicenseCheck.CommandElements[$Index + 1] -isnot [System.Management.Automation.Language.CommandParameterAst]) { + $LicenseCheck.CommandElements[$Index + 1] + } + + if (!$Argument) { + continue + } + + switch ($Element.ParameterName) { + 'Preset' { + foreach ($PresetName in (Get-StringValuesFromAst -ArgumentAst $Argument)) { + foreach ($Capability in $CapabilityPresets[$PresetName]) { + $Capabilities.Add($Capability) + } + } + } + 'RequiredCapabilities' { + foreach ($Capability in (Get-StringValuesFromAst -ArgumentAst $Argument)) { + $Capabilities.Add($Capability) + } + } + } + } + + return @($Capabilities | Where-Object { $_ } | Select-Object -Unique) +} # Find the paths to the standards.json file based on the current script path -$StandardsJSONPath = Split-Path (Split-Path $PSScriptRoot) +$CIPPApiRoot = Split-Path $PSScriptRoot +$CapabilityPresets = Get-CIPPCapabilityPresets -Path (Join-Path $CIPPApiRoot 'Modules\CIPPCore\Public\Functions\Test-CIPPStandardLicense.ps1') + +$StandardsJSONPath = Split-Path $CIPPApiRoot $StandardsJSONPath = Resolve-Path "$StandardsJSONPath\*\src\data\standards.json" $StandardsInfo = Get-Content -Path $StandardsJSONPath | ConvertFrom-Json -Depth 10 @@ -57,7 +185,7 @@ foreach ($Standard in $StandardsInfo) { # Calculate the standards file name and path $StandardFileName = $Standard.name -replace 'standards.', 'Invoke-CIPPStandard' - $StandardsFilePath = Resolve-Path "$(Split-Path $PSScriptRoot)\Modules\CIPPStandards\Public\Standards\$StandardFileName.ps1" + $StandardsFilePath = Resolve-Path "$CIPPApiRoot\Modules\CIPPStandards\Public\Standards\$StandardFileName.ps1" if (-not (Test-Path $StandardsFilePath)) { Write-Host "No file found for standard $($Standard.name)" -ForegroundColor Yellow continue @@ -120,20 +248,15 @@ foreach ($Standard in $StandardsInfo) { } - # Extract RequiredCapabilities from Test-CIPPStandardLicense in the function body - # Match the first occurrence of -RequiredCapabilities @(...) in the file - $CapabilitiesRegex = 'Test-CIPPStandardLicense\s[^}]*-RequiredCapabilities\s+@\(([^)]+)\)' - if ($Content -match $CapabilitiesRegex) { - $RawCapabilities = $Matches[1] - $Capabilities = @($RawCapabilities -split ',' | ForEach-Object { $_.Trim().Trim("'").Trim('"') } | Where-Object { $_ }) - if ($Capabilities.Count -gt 0) { - $NewComment.Add(" REQUIREDCAPABILITIES`n") - foreach ($Cap in $Capabilities) { - $NewComment.Add(" `"$Cap`"`n") - } - # Update the standard object for JSON output - $Standard | Add-Member -NotePropertyName 'requiredCapabilities' -NotePropertyValue $Capabilities -Force + $Capabilities = Get-LicenseCheckCapabilities -Content $Content -CapabilityPresets $CapabilityPresets + if ($Capabilities.Count -gt 0) { + $NewComment.Add(" REQUIREDCAPABILITIES`n") + foreach ($Cap in $Capabilities) { + $NewComment.Add(" `"$Cap`"`n") } + + # Update the standard object for JSON output + $Standard | Add-Member -NotePropertyName 'requiredCapabilities' -NotePropertyValue $Capabilities -Force } else { # No license check — remove stale property if present if ($Standard.PSObject.Properties['requiredCapabilities']) { diff --git a/host.json b/host.json index 473ab1bfce11..5635adf20ae2 100644 --- a/host.json +++ b/host.json @@ -16,7 +16,7 @@ "distributedTracingEnabled": false, "version": "None" }, - "defaultVersion": "10.4.4", + "defaultVersion": "10.4.5", "versionMatchStrategy": "Strict", "versionFailureStrategy": "Fail" } diff --git a/version_latest.txt b/version_latest.txt index 622a6f75b792..aa725fbb1929 100644 --- a/version_latest.txt +++ b/version_latest.txt @@ -1 +1 @@ -10.4.4 +10.4.5